regex: Initial regex implementation

regex: All tests are passed.
This commit is contained in:
Ilya Matveev
2017-06-07 15:51:43 +07:00
committed by ilmat192
parent 4f251003ef
commit 1bf9b23108
57 changed files with 11530 additions and 976 deletions
@@ -0,0 +1,53 @@
import kotlin.text.*
import kotlin.test.*
fun assertTrue(msg: String, value: Boolean) = assertTrue(value, msg)
fun assertFalse(msg: String, value: Boolean) = assertFalse(value, msg)
fun codePointToString(codePoint: Int): String {
val charArray = Char.toChars(codePoint)
return fromCharArray(charArray, 0, charArray.size)
}
fun box() {}
// TODO: Here is a performance problem: an execution of this test requires much more time than it in Kotlin/JVM.
fun box1() {
// Regression for HARMONY-3145
var p = Regex("(\\p{all})+")
var res = true
var cnt = 0
var s: String
for (i in 0..1114111) {
if (i % 200000 == 0) {
println(i)
}
s = codePointToString(i)
// if (!s.matches(p.toString().toRegex())) { TODO: Uncomment when caching is done.
if (!s.matches(p)) {
cnt++
res = false
}
}
assertTrue(res)
assertEquals(0, cnt)
p = Regex("(\\P{all})+")
res = true
cnt = 0
for (i in 0..1114111) {
if (i % 200000 == 0) {
println(i)
}
s = codePointToString(i)
// if (!s.matches(p.toString().toRegex())) { TODO: Uncomment when caching is done.
if (!s.matches(p)) {
cnt++
res = false
}
}
assertFalse(res)
assertEquals(0x110000, cnt)
}
@@ -18,6 +18,9 @@
import kotlin.text.*
import kotlin.test.*
fun assertTrue(msg: String, value: Boolean) = assertTrue(value, msg)
fun assertFalse(msg: String, value: Boolean) = assertFalse(value, msg)
internal var testPatterns = arrayOf("(a|b)*abb", "(1*2*3*4*)*567", "(a|b|c|d)*aab", "(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)*", "(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ)*", "(a|b)*(a|b)*A(a|b)*lice.*", "(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)(a|b|c|d|e|f|g|h|" + "i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*(1|2|3|4|5|6|7|8|9|0)*|while|for|struct|if|do")
internal var groupPatterns = arrayOf("(a|b)*aabb", "((a)|b)*aabb", "((a|b)*)a(abb)", "(((a)|(b))*)aabb", "(((a)|(b))*)aa(b)b", "(((a)|(b))*)a(a(b)b)")
@@ -78,7 +81,7 @@ fun testGroupint() {
val regex = Regex(groupPatterns[i])
val result = regex.matchEntire(positiveTestString)!!
for (j in 0..groupResults[i].size - 1) {
assertEquals("i: $i j: $j", groupResults[i][j], result.groupValues[j + 1])
assertEquals(groupResults[i][j], result.groupValues[j + 1], "i: $i j: $j")
}
}
@@ -411,42 +414,6 @@ fun testFindDollar() {
assertEquals("a", result!!.groupValues[0])
}
fun testAllCodePoints() {
// Regression for HARMONY-3145
val codePoint = IntArray(1)
var p = Regex("(\\p{all})+")
var res = true
var cnt = 0
var s: String
for (i in 0..1114111) {
codePoint[0] = i
s = String(codePoint, 0, 1)
if (!s.matches(p.toString().toRegex())) {
cnt++
res = false
}
}
assertTrue(res)
assertEquals(0, cnt)
p = Regex("(\\P{all})+")
res = true
cnt = 0
for (i in 0..1114111) {
codePoint[0] = i
s = String(codePoint, 0, 1)
if (!s.matches(p.toString().toRegex())) {
cnt++
res = false
}
}
assertFalse(res)
assertEquals(0x110000, cnt)
}
/*
* Regression test for HARMONY-674
*/
@@ -501,7 +468,6 @@ fun box() {
testUnicodeCategory()
testSplitEmpty()
testFindDollar()
testAllCodePoints()
testPatternMatcher()
test3360()
testGeneralPunctuationCategory()
@@ -22,77 +22,77 @@ fun testErrorConditions2() {
val regex = Regex("(foo[0-9])(bar[a-z])")
var result = regex.find("foo1barzfoo2baryfoozbar5")
Assert.assertTrue(result != null)
Assert.assertEquals(0, result!!.range.start)
Assert.assertEquals(7, result.range.endInclusive)
Assert.assertEquals(0, result.groups[0]!!.range.start)
Assert.assertEquals(7, result.groups[0]!!.range.endInclusive)
Assert.assertEquals(0, result.groups[1]!!.range.start)
Assert.assertEquals(3, result.groups[1]!!.range.endInclusive)
Assert.assertEquals(4, result.groups[2]!!.range.start)
Assert.assertEquals(7, result.groups[2]!!.range.endInclusive)
assertTrue(result != null)
assertEquals(0, result!!.range.start)
assertEquals(7, result.range.endInclusive)
assertEquals(0, result.groups[0]!!.range.start)
assertEquals(7, result.groups[0]!!.range.endInclusive)
assertEquals(0, result.groups[1]!!.range.start)
assertEquals(3, result.groups[1]!!.range.endInclusive)
assertEquals(4, result.groups[2]!!.range.start)
assertEquals(7, result.groups[2]!!.range.endInclusive)
try {
result.groups[3]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groupValues[3]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groups[-1]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groupValues[-1]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
result = result.next()
Assert.assertTrue(result != null)
Assert.assertEquals(8, result!!.range.start)
Assert.assertEquals(15, result.range.endInclusive)
Assert.assertEquals(8, result.groups[0]!!.range.start)
Assert.assertEquals(15, result.groups[0]!!.range.endInclusive)
Assert.assertEquals(8, result.groups[1]!!.range.start)
Assert.assertEquals(11, result.groups[1]!!.range.endInclusive)
Assert.assertEquals(12, result.groups[2]!!.range.start)
Assert.assertEquals(15, result.groups[2]!!.range.endInclusive)
assertTrue(result != null)
assertEquals(8, result!!.range.start)
assertEquals(15, result.range.endInclusive)
assertEquals(8, result.groups[0]!!.range.start)
assertEquals(15, result.groups[0]!!.range.endInclusive)
assertEquals(8, result.groups[1]!!.range.start)
assertEquals(11, result.groups[1]!!.range.endInclusive)
assertEquals(12, result.groups[2]!!.range.start)
assertEquals(15, result.groups[2]!!.range.endInclusive)
try {
result.groups[3]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groupValues[3]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groups[-1]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
try {
result.groupValues[-1]
Assert.fail("IndexOutOfBoundsException expected")
fail("IndexOutOfBoundsException expected")
} catch (e: IndexOutOfBoundsException) {
}
result = result.next()
Assert.assertFalse(result != null)
assertFalse(result != null)
}
/*
@@ -105,7 +105,7 @@ fun testReplacementBackSlash() {
val regex = Regex(replacedString)
try {
regex.replace(str, substitutionString)
Assert.fail("IllegalArgumentException should be thrown")
fail("IllegalArgumentException should be thrown")
} catch (e: IllegalArgumentException) {
}
}
@@ -26,27 +26,27 @@ fun testCase() {
regex = Regex("([a-z]+)[0-9]+")
result = regex.find("cAT123#dog345")
Assert.assertNotNull(result)
Assert.assertEquals("dog", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("dog", result!!.groupValues[1])
assertNull(result.next())
regex = Regex("([a-z]+)[0-9]+", RegexOption.IGNORE_CASE)
result = regex.find("cAt123#doG345")
Assert.assertNotNull(result)
Assert.assertEquals("cAt", result!!.groupValues[1])
assertNotNull(result)
assertEquals("cAt", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("doG", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("doG", result!!.groupValues[1])
assertNull(result.next())
regex = Regex("(?i)([a-z]+)[0-9]+")
result = regex.find("cAt123#doG345")
Assert.assertNotNull(result)
Assert.assertEquals("cAt", result!!.groupValues[1])
assertNotNull(result)
assertEquals("cAt", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("doG", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("doG", result!!.groupValues[1])
assertNull(result.next())
}
fun testMultiline() {
@@ -55,59 +55,59 @@ fun testMultiline() {
regex = Regex("^foo")
result = regex.find("foobar")
Assert.assertNotNull(result)
Assert.assertTrue(result!!.range.start == 0 && result.range.endInclusive == 2)
Assert.assertTrue(result.groups[0]!!.range.start == 0 && result.groups[0]!!.range.endInclusive == 2)
Assert.assertNull(result.next())
assertNotNull(result)
assertTrue(result!!.range.start == 0 && result.range.endInclusive == 2)
assertTrue(result.groups[0]!!.range.start == 0 && result.groups[0]!!.range.endInclusive == 2)
assertNull(result.next())
result = regex.find("barfoo")
Assert.assertNull(result)
assertNull(result)
regex = Regex("foo$")
result = regex.find("foobar")
Assert.assertNull(result)
assertNull(result)
result = regex.find("barfoo")
Assert.assertNotNull(result)
Assert.assertTrue(result!!.range.start == 3 && result.range.endInclusive == 5)
Assert.assertTrue(result.groups[0]!!.range.start == 3 && result.groups[0]!!.range.endInclusive == 5)
Assert.assertNull(result.next())
assertNotNull(result)
assertTrue(result!!.range.start == 3 && result.range.endInclusive == 5)
assertTrue(result.groups[0]!!.range.start == 3 && result.groups[0]!!.range.endInclusive == 5)
assertNull(result.next())
regex = Regex("^foo([0-9]*)", RegexOption.MULTILINE)
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
Assert.assertNotNull(result)
Assert.assertEquals("1", result!!.groupValues[1])
assertNotNull(result)
assertEquals("1", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("2", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("2", result!!.groupValues[1])
assertNull(result.next())
regex = Regex("foo([0-9]*)$", RegexOption.MULTILINE)
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
Assert.assertNotNull(result)
Assert.assertEquals("3", result!!.groupValues[1])
assertNotNull(result)
assertEquals("3", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("4", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("4", result!!.groupValues[1])
assertNull(result.next())
regex = Regex("(?m)^foo([0-9]*)")
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
Assert.assertNotNull(result)
Assert.assertEquals("1", result!!.groupValues[1])
assertNotNull(result)
assertEquals("1", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("2", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("2", result!!.groupValues[1])
assertNull(result.next())
regex = Regex("(?m)foo([0-9]*)$")
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
Assert.assertNotNull(result)
Assert.assertEquals("3", result!!.groupValues[1])
assertNotNull(result)
assertEquals("3", result!!.groupValues[1])
result = result.next()
Assert.assertNotNull(result)
Assert.assertEquals("4", result!!.groupValues[1])
Assert.assertNull(result.next())
assertNotNull(result)
assertEquals("4", result!!.groupValues[1])
assertNull(result.next())
}
fun box() {
@@ -21,7 +21,7 @@ fun testCase() {
val regex = "("
try {
Regex(regex)
Assert.fail("PatternSyntaxException expected")
fail("PatternSyntaxException expected")
} catch (e: PatternSyntaxException) {
// TODO: Check the exception's properties.
}
@@ -32,7 +32,7 @@ fun testCase2() {
val regex = "[4-"
try {
Regex(regex)
Assert.fail("PatternSyntaxException expected")
fail("PatternSyntaxException expected")
} catch (e: PatternSyntaxException) {
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -28,8 +28,8 @@ fun testSimpleReplace() {
val regex = Regex(pattern)
Assert.assertEquals("foobarxxxarfoofo1", regex.replaceFirst(target, repl))
Assert.assertEquals("foobarxxxarfooxxx", regex.replace(target, repl))
assertEquals("foobarxxxarfoofo1", regex.replaceFirst(target, repl))
assertEquals("foobarxxxarfooxxx", regex.replace(target, repl))
}
fun testCaptureReplace() {
@@ -45,18 +45,18 @@ fun testCaptureReplace() {
regex = Regex(pattern)
s = regex.replaceFirst(target, repl)
Assert.assertEquals("foo[31];bar[42];[99]xyz", s)
assertEquals("foo[31];bar[42];[99]xyz", s)
s = regex.replace(target, repl)
Assert.assertEquals("foo[31];bar[42];xyz[99]", s)
assertEquals("foo[31];bar[42];xyz[99]", s)
target = "[31]foo(42)bar{63}zoo;[12]abc(34)def{56}ghi;{99}xyz[88]xyz(77)xyz;"
pattern = "\\[([0-9]+)\\]([a-z]+)\\(([0-9]+)\\)([a-z]+)\\{([0-9]+)\\}([a-z]+)"
repl = "[$5]$6($3)$4{$1}$2"
regex = Regex(pattern)
s = regex.replaceFirst(target, repl)
Assert.assertEquals("[63]zoo(42)bar{31}foo;[12]abc(34)def{56}ghi;{99}xyz[88]xyz(77)xyz;", s)
assertEquals("[63]zoo(42)bar{31}foo;[12]abc(34)def{56}ghi;{99}xyz[88]xyz(77)xyz;", s)
s = regex.replace(target, repl)
Assert.assertEquals("[63]zoo(42)bar{31}foo;[56]ghi(34)def{12}abc;{99}xyz[88]xyz(77)xyz;", s)
assertEquals("[63]zoo(42)bar{31}foo;[56]ghi(34)def{12}abc;{99}xyz[88]xyz(77)xyz;", s)
}
fun testEscapeReplace() {
@@ -69,13 +69,13 @@ fun testEscapeReplace() {
pattern = "'"
repl = "\\'"
s = target.replace(pattern.toRegex(), repl)
Assert.assertEquals("foo'bar''foo", s)
assertEquals("foo'bar''foo", s)
repl = "\\\\'"
s = target.replace(pattern.toRegex(), repl)
Assert.assertEquals("foo\\'bar\\'\\'foo", s)
assertEquals("foo\\'bar\\'\\'foo", s)
repl = "\\$3"
s = target.replace(pattern.toRegex(), repl)
Assert.assertEquals("foo$3bar$3$3foo", s)
assertEquals("foo$3bar$3$3foo", s)
}
fun box() {
@@ -22,13 +22,12 @@ fun testSimple() {
val p = Regex("/")
val results = p.split("have/you/done/it/right")
val expected = arrayOf("have", "you", "done", "it", "right")
Assert.assertEquals(expected.size, results.size)
assertEquals(expected.size, results.size)
for (i in expected.indices) {
Assert.assertEquals(results[i], expected[i])
assertEquals(results[i], expected[i])
}
}
@Throws(PatternSyntaxException::class)
fun testSplit1() {
var p = Regex(" ")
@@ -36,100 +35,100 @@ fun testSplit1() {
var tokens: List<String>
tokens = p.split(input, 1)
Assert.assertEquals(1, tokens.size)
Assert.assertTrue(tokens[0] == input)
assertEquals(1, tokens.size)
assertTrue(tokens[0] == input)
tokens = p.split(input, 2)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poodle", tokens[0])
Assert.assertEquals("zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poodle", tokens[0])
assertEquals("zoo", tokens[1])
tokens = p.split(input, 5)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poodle", tokens[0])
Assert.assertEquals("zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poodle", tokens[0])
assertEquals("zoo", tokens[1])
tokens = p.split(input, 0)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poodle", tokens[0])
Assert.assertEquals("zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poodle", tokens[0])
assertEquals("zoo", tokens[1])
tokens = p.split(input)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poodle", tokens[0])
Assert.assertEquals("zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poodle", tokens[0])
assertEquals("zoo", tokens[1])
p = Regex("d")
tokens = p.split(input, 1)
Assert.assertEquals(1, tokens.size)
Assert.assertTrue(tokens[0] == input)
assertEquals(1, tokens.size)
assertTrue(tokens[0] == input)
tokens = p.split(input, 2)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poo", tokens[0])
Assert.assertEquals("le zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poo", tokens[0])
assertEquals("le zoo", tokens[1])
tokens = p.split(input, 5)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poo", tokens[0])
Assert.assertEquals("le zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poo", tokens[0])
assertEquals("le zoo", tokens[1])
tokens = p.split(input, 0)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poo", tokens[0])
Assert.assertEquals("le zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poo", tokens[0])
assertEquals("le zoo", tokens[1])
tokens = p.split(input)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("poo", tokens[0])
Assert.assertEquals("le zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("poo", tokens[0])
assertEquals("le zoo", tokens[1])
p = Regex("o")
tokens = p.split(input, 1)
Assert.assertEquals(1, tokens.size)
Assert.assertTrue(tokens[0] == input)
assertEquals(1, tokens.size)
assertTrue(tokens[0] == input)
tokens = p.split(input, 2)
Assert.assertEquals(2, tokens.size)
Assert.assertEquals("p", tokens[0])
Assert.assertEquals("odle zoo", tokens[1])
assertEquals(2, tokens.size)
assertEquals("p", tokens[0])
assertEquals("odle zoo", tokens[1])
tokens = p.split(input, 5)
Assert.assertEquals(5, tokens.size)
Assert.assertEquals("p", tokens[0])
Assert.assertTrue(tokens[1] == "")
Assert.assertEquals("dle z", tokens[2])
Assert.assertTrue(tokens[3] == "")
Assert.assertTrue(tokens[4] == "")
assertEquals(5, tokens.size)
assertEquals("p", tokens[0])
assertTrue(tokens[1] == "")
assertEquals("dle z", tokens[2])
assertTrue(tokens[3] == "")
assertTrue(tokens[4] == "")
tokens = p.split(input, 0)
Assert.assertEquals(5, tokens.size)
Assert.assertEquals("p", tokens[0])
Assert.assertTrue(tokens[1] == "")
Assert.assertEquals("dle z", tokens[2])
Assert.assertTrue(tokens[3] == "")
Assert.assertTrue(tokens[4] == "")
assertEquals(5, tokens.size)
assertEquals("p", tokens[0])
assertTrue(tokens[1] == "")
assertEquals("dle z", tokens[2])
assertTrue(tokens[3] == "")
assertTrue(tokens[4] == "")
tokens = p.split(input)
Assert.assertEquals(5, tokens.size)
Assert.assertEquals("p", tokens[0])
Assert.assertTrue(tokens[1] == "")
Assert.assertEquals("dle z", tokens[2])
Assert.assertTrue(tokens[3] == "")
Assert.assertTrue(tokens[4] == "")
assertEquals(5, tokens.size)
assertEquals("p", tokens[0])
assertTrue(tokens[1] == "")
assertEquals("dle z", tokens[2])
assertTrue(tokens[3] == "")
assertTrue(tokens[4] == "")
}
fun testSplit2() {
val p = Regex("")
var s: List<String>
s = p.split("a", 0)
Assert.assertEquals(3, s.size)
Assert.assertEquals("", s[0])
Assert.assertEquals("a", s[1])
Assert.assertEquals("", s[2])
assertEquals(3, s.size)
assertEquals("", s[0])
assertEquals("a", s[1])
assertEquals("", s[2])
s = p.split("", 0)
Assert.assertEquals(1, s.size)
Assert.assertEquals("", s[0])
assertEquals(1, s.size)
assertEquals("", s[0])
s = p.split("abcd", 0)
Assert.assertEquals(6, s.size)
Assert.assertEquals("", s[0])
Assert.assertEquals("a", s[1])
Assert.assertEquals("b", s[2])
Assert.assertEquals("c", s[3])
Assert.assertEquals("d", s[4])
Assert.assertEquals("", s[5])
assertEquals(6, s.size)
assertEquals("", s[0])
assertEquals("a", s[1])
assertEquals("b", s[2])
assertEquals("c", s[3])
assertEquals("d", s[4])
assertEquals("", s[5])
}
fun testSplitSupplementaryWithEmptyString() {
@@ -141,12 +140,12 @@ fun testSplitSupplementaryWithEmptyString() {
val p = Regex("")
val s: List<String>
s = p.split("a\ud869\uded6b", 0)
Assert.assertEquals(5, s.size)
Assert.assertEquals("", s[0])
Assert.assertEquals("a", s[1])
Assert.assertEquals("\ud869\uded6", s[2])
Assert.assertEquals("b", s[3])
Assert.assertEquals("", s[4])
assertEquals(5, s.size)
assertEquals("", s[0])
assertEquals("a", s[1])
assertEquals("\ud869\uded6", s[2])
assertEquals("b", s[3])
assertEquals("", s[4])
}
fun box() {
+31 -18
View File
@@ -647,35 +647,31 @@ public fun String.replaceBeforeLast(delimiter: String, replacement: String, miss
* The [replacement] can consist of any combination of literal text and $-substitutions. To treat the replacement string
* literally escape it with the [kotlin.text.Regex.Companion.escapeReplacement] method.
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline fun CharSequence.replace(regex: Regex, replacement: String): String = regex.replace(this, replacement)
@kotlin.internal.InlineOnly
public inline fun CharSequence.replace(regex: Regex, replacement: String): String = regex.replace(this, replacement)
/**
* Returns a new string obtained by replacing each substring of this char sequence that matches the given regular expression
* with the result of the given function [transform] that takes [MatchResult] and returns a string to be used as a
* replacement for that match.
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace(this, transform)
@kotlin.internal.InlineOnly
public inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace(this, transform)
/**
* Replaces the first occurrence of the given regular expression [regex] in this char sequence with specified [replacement] expression.
*
* @param replacement A replacement expression that can include substitutions. See [Regex.replaceFirst] for details.
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline fun CharSequence.replaceFirst(regex: Regex, replacement: String): String = regex.replaceFirst(this, replacement)
@kotlin.internal.InlineOnly
public inline fun CharSequence.replaceFirst(regex: Regex, replacement: String): String = regex.replaceFirst(this, replacement)
/**
* Returns `true` if this char sequence matches the given regular expression.
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline fun CharSequence.matches(regex: Regex): Boolean = regex.matches(this)
@kotlin.internal.InlineOnly
public inline fun CharSequence.matches(regex: Regex): Boolean = regex.matches(this)
/**
* Implementation of [regionMatches] for CharSequences.
@@ -1017,9 +1013,8 @@ public operator fun CharSequence.contains(char: Char, ignoreCase: Boolean = fals
/**
* Returns `true` if this char sequence contains at least one match of the specified regular expression [regex].
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline operator fun CharSequence.contains(regex: Regex): Boolean = regex.containsMatchIn(this)
@kotlin.internal.InlineOnly
public inline operator fun CharSequence.contains(regex: Regex): Boolean = regex.containsMatchIn(this)
// rangesDelimitedBy
@@ -1177,9 +1172,8 @@ public fun CharSequence.split(vararg delimiters: Char, ignoreCase: Boolean = fal
* @param limit Non-negative value specifying the maximum number of substrings to return.
* Zero by default means no limit is set.
*/
//@FixmeRegex
//@kotlin.internal.InlineOnly
//public inline fun CharSequence.split(regex: Regex, limit: Int = 0): List<String> = regex.split(this, limit)
@kotlin.internal.InlineOnly
public inline fun CharSequence.split(regex: Regex, limit: Int = 0): List<String> = regex.split(this, limit)
/**
* Splits this char sequence to a sequence of lines delimited by any of the following character sequences: CRLF, LF or CR.
@@ -2391,3 +2385,22 @@ public fun CharSequence.repeat(n: Int): String {
}
}
}
/**
* Converts the string into a regular expression [Regex] with the default options.
*/
@kotlin.internal.InlineOnly
public inline fun String.toRegex(): Regex = Regex(this)
/**
* Converts the string into a regular expression [Regex] with the specified single [option].
*/
@kotlin.internal.InlineOnly
public inline fun String.toRegex(option: RegexOption): Regex = Regex(this, option)
/**
* Converts the string into a regular expression [Regex] with the specified set of [options].
*/
@kotlin.internal.InlineOnly
public inline fun String.toRegex(options: Set<RegexOption>): Regex = Regex(this, options)
@@ -0,0 +1,553 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Unicode category (i.e. Ll, Lu).
* @author Nikolay A. Kuznetsov
*/
internal open class UnicodeCategory(protected val category: Int) : AbstractCharClass() {
override fun contains(ch: Int): Boolean = alt xor (ch.toChar().category.value == category)
}
/**
* Unicode category scope (i.e IsL, IsM, ...)
* @author Nikolay A. Kuznetsov
*/
internal class UnicodeCategoryScope(category: Int) : UnicodeCategory(category) {
override fun contains(ch: Int): Boolean = alt xor (category shr ch.toChar().category.value and 1 != 0)
}
/**
* This class represents character classes, i.e. sets of character either predefined or user defined.
* Note: this class represent a token, not node, so being constructed by lexer.
*
* @author Nikolay A. Kuznetsov
*/
internal abstract class AbstractCharClass : SpecialToken() {
/**
* Show if the class has alternative meaning:
* if the class contains character 'a' and alt == true then the class will contains all characters except 'a'.
*/
internal var alt: Boolean = false
internal var altSurrogates: Boolean = false
internal val lowHighSurrogates = BitSet(SURROGATE_CARDINALITY) // Bit set for surrogates?
/*
* Indicates if this class may contain supplementary Unicode codepoints.
* If this flag is specified it doesn't mean that this class contains supplementary characters but may contain.
*/
var mayContainSupplCodepoints = false
protected set
/** Returns true if this char class contains character specified. */
abstract operator fun contains(ch: Int): Boolean
open fun contains(ch: Char): Boolean = contains(ch.toInt())
/**
* Returns BitSet representing this character class or `null`
* if this character class does not have character representation;
*/
// TODO: @C++?. Or implement a BitSet
open internal val bits: BitSet?
get() = null
fun hasLowHighSurrogates(): Boolean {
return if (altSurrogates)
lowHighSurrogates.nextClearBit(0) != -1 // TODO: What if the bitset is empty?
else
lowHighSurrogates.nextSetBit(0) != -1
}
override val type: Type = Type.CHARCLASS
open val instance: AbstractCharClass
get() = this
// TODO: refactor
val surrogates: AbstractCharClass by lazy {
val result = object : AbstractCharClass() {
override fun contains(ch: Int): Boolean {
val index = ch - Char.MIN_SURROGATE.toInt()
return if (index >= 0 && index < AbstractCharClass.SURROGATE_CARDINALITY)
this.altSurrogates xor this@AbstractCharClass.lowHighSurrogates.get(index)
else
false
}
}
result.setNegative(this.altSurrogates)
return@lazy result
}
// TODO: refactor
val withoutSurrogates: AbstractCharClass by lazy {
val result = object : AbstractCharClass() {
override fun contains(ch: Int): Boolean {
val index = ch - Char.MIN_SURROGATE.toInt()
val containslHS = if (index >= 0 && index < AbstractCharClass.SURROGATE_CARDINALITY)
this.altSurrogates xor this@AbstractCharClass.lowHighSurrogates.get(index)
else
false
return this@AbstractCharClass.contains(ch) && !containslHS
}
}
result.setNegative(isNegative())
result.mayContainSupplCodepoints = mayContainSupplCodepoints
return@lazy result
}
/**
* Sets this CharClass to negative form, i.e. if they will add some characters and after that set this
* class to negative it will accept all the characters except previously set ones.
*
* Although this method will not alternate all the already set characters,
* just overall meaning of the class.
*/
// TODO: replace with a property
fun setNegative(value: Boolean): AbstractCharClass {
if (alt xor value) {
alt = !alt
altSurrogates = !altSurrogates
}
if (!mayContainSupplCodepoints) {
mayContainSupplCodepoints = true
}
return this
}
fun isNegative(): Boolean {
return alt
}
// TODO: replace getValue with property access?
internal abstract class LazyCharClass {
private val posValue: AbstractCharClass by lazy { computeValue() }
private val negValue: AbstractCharClass by lazy { computeValue().setNegative(true) }
fun getValue(negative: Boolean): AbstractCharClass = if (!negative) posValue else negValue
protected abstract fun computeValue(): AbstractCharClass
}
internal class LazyDigit : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('0', '9')
}
}
// TODO: DO we need it? But we have LazyCharCLass.negValue. May be it's connected with mayContainSupplCodepoints?
// TODO: Don't use bitmaps in preset classes?
internal class LazyNonDigit : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
val result = CharClass().add('0', '9').setNegative(true)
result.mayContainSupplCodepoints = true
return result
}
}
internal class LazySpace : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
/* 9-13 - \t\n\x0B\f\r; 32 - ' ' */
return CharClass().add(9, 13).add(32)
}
}
internal class LazyNonSpace : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
val result = LazySpace().getValue(negative = true)
result.mayContainSupplCodepoints = true
return result
}
}
internal class LazyWord : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('a', 'z').add('A', 'Z').add('0', '9')
.add('_')
}
}
internal class LazyNonWord : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
val result = LazyWord().getValue(negative = true)
result.mayContainSupplCodepoints = true
return result
}
}
// TODO: It is only for latin in Harmony.
internal class LazyLower : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('a', 'z')
}
}
internal class LazyUpper : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('A', 'Z')
}
}
internal class LazyASCII : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add(0x00, 0x7F)
}
}
internal class LazyAlpha : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('a', 'z').add('A', 'Z')
}
}
internal class LazyAlnum : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
// TODO: Get rid of the cast?
return (LazyAlpha().getValue(negative = false) as CharClass).add('0', '9')
}
}
internal class LazyPunct : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
/* Punctuation !"#$%&'()*+,-./:;<=>?@ [\]^_` {|}~ */
return CharClass().add(0x21, 0x40).add(0x5B, 0x60).add(0x7B,
0x7E)
}
}
internal class LazyGraph : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
/* plus punctuation */
return (LazyAlnum().getValue(negative = false) as CharClass)
.add(0x21, 0x40)
.add(0x5B, 0x60)
.add(0x7B, 0x7E)
}
}
internal class LazyPrint : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return (LazyGraph().getValue(negative = true) as CharClass).add(0x20)
}
}
internal class LazyBlank : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add(' ').add('\t')
}
}
internal class LazyCntrl : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add(0x00, 0x1F).add(0x7F)
}
}
internal class LazyXDigit : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
return CharClass().add('0', '9').add('a', 'f').add('A', 'F')
}
}
internal class LazyRange(var start: Int, var end: Int) : LazyCharClass() {
public override fun computeValue(): AbstractCharClass {
val chCl = CharClass().add(start, end)
return chCl
}
}
internal class LazySpecialsBlock : LazyCharClass() {
public override fun computeValue(): AbstractCharClass {
return CharClass().add(0xFEFF, 0xFEFF).add(0xFFF0, 0xFFFD)
}
}
internal class LazyCategoryScope(
val category: Int,
val mayContainSupplCodepoints: Boolean,
val containsAllSurrogates: Boolean = false) : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
val result = UnicodeCategoryScope(category)
if (containsAllSurrogates) {
result.lowHighSurrogates.set(0, SURROGATE_CARDINALITY)
}
result.mayContainSupplCodepoints = mayContainSupplCodepoints
return result
}
}
internal class LazyCategory(
val category: Int,
val mayContainSupplCodepoints: Boolean,
val containsAllSurrogates: Boolean = false) : LazyCharClass() {
override fun computeValue(): AbstractCharClass {
val result = UnicodeCategory(category)
if (containsAllSurrogates) {
result.lowHighSurrogates.set(0, SURROGATE_CARDINALITY)
}
result.mayContainSupplCodepoints = mayContainSupplCodepoints
return result
}
}
// -----------------------------------------------------------------
// Static methods and predefined classes
// -----------------------------------------------------------------
companion object {
//Char.MAX_SURROGATE - Char.MIN_SURROGATE + 1
var SURROGATE_CARDINALITY = 2048
var space: LazyCharClass = LazySpace()
var digit: LazyCharClass = LazyDigit()
/**
* character classes generated from
* http://www.unicode.org/reports/tr18/
* http://www.unicode.org/Public/4.1.0/ucd/Blocks.txt
*/
// TODO: @C++ Harmony uses ListResourceBundle class here. TODO: perfrom a effector lookup on the C++ side.
// Temporary solution: when
// TODO: @C++. Or make it in code. Or do something else.
// We can lazily create objects of char classes in C++ hashmap or Array.
// The main point of it is laziness
fun getClass(name: String) : LazyCharClass {
return when (name) {
"Lower" -> LazyLower()
"Upper" -> LazyUpper()
"ASCII" -> LazyASCII()
"Alpha" -> LazyAlpha()
"Digit" -> digit
"Alnum" -> LazyAlnum()
"Punct" -> LazyPunct()
"Graph" -> LazyGraph()
"Print" -> LazyPrint()
"Blank" -> LazyBlank()
"Cntrl" -> LazyCntrl()
"XDigit" -> LazyXDigit()
"Space" -> space
"w" -> LazyWord()
"W" -> LazyNonWord()
"s" -> space
"S" -> LazyNonSpace()
"d" -> digit
"D" -> LazyNonDigit()
"BasicLatin" -> LazyRange(0x0000, 0x007F)
"Latin-1Supplement" -> LazyRange(0x0080, 0x00FF)
"LatinExtended-A" -> LazyRange(0x0100, 0x017F)
"LatinExtended-B" -> LazyRange(0x0180, 0x024F)
"IPAExtensions" -> LazyRange(0x0250, 0x02AF)
"SpacingModifierLetters" -> LazyRange(0x02B0, 0x02FF)
"CombiningDiacriticalMarks" -> LazyRange(0x0300, 0x036F)
"Greek" -> LazyRange(0x0370, 0x03FF)
"Cyrillic" -> LazyRange(0x0400, 0x04FF)
"CyrillicSupplement" -> LazyRange(0x0500, 0x052F)
"Armenian" -> LazyRange(0x0530, 0x058F)
"Hebrew" -> LazyRange(0x0590, 0x05FF)
"Arabic" -> LazyRange(0x0600, 0x06FF)
"Syriac" -> LazyRange(0x0700, 0x074F)
"ArabicSupplement" -> LazyRange(0x0750, 0x077F)
"Thaana" -> LazyRange(0x0780, 0x07BF)
"Devanagari" -> LazyRange(0x0900, 0x097F)
"Bengali" -> LazyRange(0x0980, 0x09FF)
"Gurmukhi" -> LazyRange(0x0A00, 0x0A7F)
"Gujarati" -> LazyRange(0x0A80, 0x0AFF)
"Oriya" -> LazyRange(0x0B00, 0x0B7F)
"Tamil" -> LazyRange(0x0B80, 0x0BFF)
"Telugu" -> LazyRange(0x0C00, 0x0C7F)
"Kannada" -> LazyRange(0x0C80, 0x0CFF)
"Malayalam" -> LazyRange(0x0D00, 0x0D7F)
"Sinhala" -> LazyRange(0x0D80, 0x0DFF)
"Thai" -> LazyRange(0x0E00, 0x0E7F)
"Lao" -> LazyRange(0x0E80, 0x0EFF)
"Tibetan" -> LazyRange(0x0F00, 0x0FFF)
"Myanmar" -> LazyRange(0x1000, 0x109F)
"Georgian" -> LazyRange(0x10A0, 0x10FF)
"HangulJamo" -> LazyRange(0x1100, 0x11FF)
"Ethiopic" -> LazyRange(0x1200, 0x137F)
"EthiopicSupplement" -> LazyRange(0x1380, 0x139F)
"Cherokee" -> LazyRange(0x13A0, 0x13FF)
"UnifiedCanadianAboriginalSyllabics" -> LazyRange(0x1400, 0x167F)
"Ogham" -> LazyRange(0x1680, 0x169F)
"Runic" -> LazyRange(0x16A0, 0x16FF)
"Tagalog" -> LazyRange(0x1700, 0x171F)
"Hanunoo" -> LazyRange(0x1720, 0x173F)
"Buhid" -> LazyRange(0x1740, 0x175F)
"Tagbanwa" -> LazyRange(0x1760, 0x177F)
"Khmer" -> LazyRange(0x1780, 0x17FF)
"Mongolian" -> LazyRange(0x1800, 0x18AF)
"Limbu" -> LazyRange(0x1900, 0x194F)
"TaiLe" -> LazyRange(0x1950, 0x197F)
"NewTaiLue" -> LazyRange(0x1980, 0x19DF)
"KhmerSymbols" -> LazyRange(0x19E0, 0x19FF)
"Buginese" -> LazyRange(0x1A00, 0x1A1F)
"PhoneticExtensions" -> LazyRange(0x1D00, 0x1D7F)
"PhoneticExtensionsSupplement" -> LazyRange(0x1D80, 0x1DBF)
"CombiningDiacriticalMarksSupplement" -> LazyRange(0x1DC0, 0x1DFF)
"LatinExtendedAdditional" -> LazyRange(0x1E00, 0x1EFF)
"GreekExtended" -> LazyRange(0x1F00, 0x1FFF)
"GeneralPunctuation" -> LazyRange(0x2000, 0x206F)
"SuperscriptsandSubscripts" -> LazyRange(0x2070, 0x209F)
"CurrencySymbols" -> LazyRange(0x20A0, 0x20CF)
"CombiningMarksforSymbols" -> LazyRange(0x20D0, 0x20FF)
"LetterlikeSymbols" -> LazyRange(0x2100, 0x214F)
"NumberForms" -> LazyRange(0x2150, 0x218F)
"Arrows" -> LazyRange(0x2190, 0x21FF)
"MathematicalOperators" -> LazyRange(0x2200, 0x22FF)
"MiscellaneousTechnical" -> LazyRange(0x2300, 0x23FF)
"ControlPictures" -> LazyRange(0x2400, 0x243F)
"OpticalCharacterRecognition" -> LazyRange(0x2440, 0x245F)
"EnclosedAlphanumerics" -> LazyRange(0x2460, 0x24FF)
"BoxDrawing" -> LazyRange(0x2500, 0x257F)
"BlockElements" -> LazyRange(0x2580, 0x259F)
"GeometricShapes" -> LazyRange(0x25A0, 0x25FF)
"MiscellaneousSymbols" -> LazyRange(0x2600, 0x26FF)
"Dingbats" -> LazyRange(0x2700, 0x27BF)
"MiscellaneousMathematicalSymbols-A" -> LazyRange(0x27C0, 0x27EF)
"SupplementalArrows-A" -> LazyRange(0x27F0, 0x27FF)
"BraillePatterns" -> LazyRange(0x2800, 0x28FF)
"SupplementalArrows-B" -> LazyRange(0x2900, 0x297F)
"MiscellaneousMathematicalSymbols-B" -> LazyRange(0x2980, 0x29FF)
"SupplementalMathematicalOperators" -> LazyRange(0x2A00, 0x2AFF)
"MiscellaneousSymbolsandArrows" -> LazyRange(0x2B00, 0x2BFF)
"Glagolitic" -> LazyRange(0x2C00, 0x2C5F)
"Coptic" -> LazyRange(0x2C80, 0x2CFF)
"GeorgianSupplement" -> LazyRange(0x2D00, 0x2D2F)
"Tifinagh" -> LazyRange(0x2D30, 0x2D7F)
"EthiopicExtended" -> LazyRange(0x2D80, 0x2DDF)
"SupplementalPunctuation" -> LazyRange(0x2E00, 0x2E7F)
"CJKRadicalsSupplement" -> LazyRange(0x2E80, 0x2EFF)
"KangxiRadicals" -> LazyRange(0x2F00, 0x2FDF)
"IdeographicDescriptionCharacters" -> LazyRange(0x2FF0, 0x2FFF)
"CJKSymbolsandPunctuation" -> LazyRange(0x3000, 0x303F)
"Hiragana" -> LazyRange(0x3040, 0x309F)
"Katakana" -> LazyRange(0x30A0, 0x30FF)
"Bopomofo" -> LazyRange(0x3100, 0x312F)
"HangulCompatibilityJamo" -> LazyRange(0x3130, 0x318F)
"Kanbun" -> LazyRange(0x3190, 0x319F)
"BopomofoExtended" -> LazyRange(0x31A0, 0x31BF)
"CJKStrokes" -> LazyRange(0x31C0, 0x31EF)
"KatakanaPhoneticExtensions" -> LazyRange(0x31F0, 0x31FF)
"EnclosedCJKLettersandMonths" -> LazyRange(0x3200, 0x32FF)
"CJKCompatibility" -> LazyRange(0x3300, 0x33FF)
"CJKUnifiedIdeographsExtensionA" -> LazyRange(0x3400, 0x4DB5)
"YijingHexagramSymbols" -> LazyRange(0x4DC0, 0x4DFF)
"CJKUnifiedIdeographs" -> LazyRange(0x4E00, 0x9FFF)
"YiSyllables" -> LazyRange(0xA000, 0xA48F)
"YiRadicals" -> LazyRange(0xA490, 0xA4CF)
"ModifierToneLetters" -> LazyRange(0xA700, 0xA71F)
"SylotiNagri" -> LazyRange(0xA800, 0xA82F)
"HangulSyllables" -> LazyRange(0xAC00, 0xD7A3)
"HighSurrogates" -> LazyRange(0xD800, 0xDB7F)
"HighPrivateUseSurrogates" -> LazyRange(0xDB80, 0xDBFF)
"LowSurrogates" -> LazyRange(0xDC00, 0xDFFF)
"PrivateUseArea" -> LazyRange(0xE000, 0xF8FF)
"CJKCompatibilityIdeographs" -> LazyRange(0xF900, 0xFAFF)
"AlphabeticPresentationForms" -> LazyRange(0xFB00, 0xFB4F)
"ArabicPresentationForms-A" -> LazyRange(0xFB50, 0xFDFF)
"VariationSelectors" -> LazyRange(0xFE00, 0xFE0F)
"VerticalForms" -> LazyRange(0xFE10, 0xFE1F)
"CombiningHalfMarks" -> LazyRange(0xFE20, 0xFE2F)
"CJKCompatibilityForms" -> LazyRange(0xFE30, 0xFE4F)
"SmallFormVariants" -> LazyRange(0xFE50, 0xFE6F)
"ArabicPresentationForms-B" -> LazyRange(0xFE70, 0xFEFF)
"HalfwidthandFullwidthForms" -> LazyRange(0xFF00, 0xFFEF)
"all" -> LazyRange(0x00, 0x10FFFF)
"Specials" -> LazySpecialsBlock()
"Cn" -> LazyCategory(CharCategory.UNASSIGNED.value, true)
"IsL" -> LazyCategoryScope(0x3E, true)
"Lu" -> LazyCategory(CharCategory.UPPERCASE_LETTER.value, true)
"Ll" -> LazyCategory(CharCategory.LOWERCASE_LETTER.value, true)
"Lt" -> LazyCategory(CharCategory.TITLECASE_LETTER.value, false)
"Lm" -> LazyCategory(CharCategory.MODIFIER_LETTER.value, false)
"Lo" -> LazyCategory(CharCategory.OTHER_LETTER.value, true)
"IsM" -> LazyCategoryScope(0x1C0, true)
"Mn" -> LazyCategory(CharCategory.NON_SPACING_MARK.value, true)
"Me" -> LazyCategory(CharCategory.ENCLOSING_MARK.value, false)
"Mc" -> LazyCategory(CharCategory.COMBINING_SPACING_MARK.value, true)
"N" -> LazyCategoryScope(0xE00, true)
"Nd" -> LazyCategory(CharCategory.DECIMAL_DIGIT_NUMBER.value, true)
"Nl" -> LazyCategory(CharCategory.LETTER_NUMBER.value, true)
"No" -> LazyCategory(CharCategory.OTHER_NUMBER.value, true)
"IsZ" -> LazyCategoryScope(0x7000, false)
"Zs" -> LazyCategory(CharCategory.SPACE_SEPARATOR.value, false)
"Zl" -> LazyCategory(CharCategory.LINE_SEPARATOR.value, false)
"Zp" -> LazyCategory(CharCategory.PARAGRAPH_SEPARATOR.value, false)
"IsC" -> LazyCategoryScope(0xF0000, true, true)
"Cc" -> LazyCategory(CharCategory.CONTROL.value, false)
"Cf" -> LazyCategory(CharCategory.FORMAT.value, true)
"Co" -> LazyCategory(CharCategory.PRIVATE_USE.value, true)
"Cs" -> LazyCategory(CharCategory.SURROGATE.value, false, true)
"IsP" -> LazyCategoryScope(1 shl CharCategory.DASH_PUNCTUATION.value or
(1 shl CharCategory.START_PUNCTUATION.value) or
(1 shl CharCategory.END_PUNCTUATION.value) or
(1 shl CharCategory.CONNECTOR_PUNCTUATION.value) or
(1 shl CharCategory.OTHER_PUNCTUATION.value) or
(1 shl CharCategory.INITIAL_QUOTE_PUNCTUATION.value) or
(1 shl CharCategory.FINAL_QUOTE_PUNCTUATION.value), true)
"Pd" -> LazyCategory(CharCategory.DASH_PUNCTUATION.value, false)
"Ps" -> LazyCategory(CharCategory.START_PUNCTUATION.value, false)
"Pe" -> LazyCategory(CharCategory.END_PUNCTUATION.value, false)
"Pc" -> LazyCategory(CharCategory.CONNECTOR_PUNCTUATION.value, false)
"Po" -> LazyCategory(CharCategory.OTHER_PUNCTUATION.value, true)
"IsS" -> LazyCategoryScope(0x7E000000, true)
"Sm" -> LazyCategory(CharCategory.MATH_SYMBOL.value, true)
"Sc" -> LazyCategory(CharCategory.CURRENCY_SYMBOL.value, false)
"Sk" -> LazyCategory(CharCategory.MODIFIER_SYMBOL.value, false)
"So" -> LazyCategory(CharCategory.OTHER_SYMBOL.value, true)
"Pi" -> LazyCategory(CharCategory.INITIAL_QUOTE_PUNCTUATION.value, false)
"Pf" -> LazyCategory(CharCategory.FINAL_QUOTE_PUNCTUATION.value, false)
else -> throw PatternSyntaxException("No such character class")
}
}
fun intersects(ch1: Int, ch2: Int): Boolean {
return ch1 == ch2
}
fun intersects(cc: AbstractCharClass, ch: Int): Boolean {
return cc.contains(ch)
}
fun intersects(cc1: AbstractCharClass,
cc2: AbstractCharClass): Boolean {
if (cc1.bits == null || cc2.bits == null)
return true
return cc1.bits!!.intersects(cc2.bits!!)
}
fun getPredefinedClass(name: String, negative: Boolean): AbstractCharClass {
return (getClass(name)).getValue(negative)
}
}
}
@@ -0,0 +1,59 @@
// TODO: License.
package kotlin.text
/**
* Line terminator factory
*
* @author Nikolay A. Kuznetsov
*/
internal abstract class AbstractLineTerminator {
/** Checks if the single character is a line terminator or not. */
open fun isLineTerminator(char: Char): Boolean = isLineTerminator(char.toInt())
/** Checks if the codepoint is a line terminator or not */
abstract fun isLineTerminator(codepoint: Int): Boolean
/** Checks if the pair of symbols is a line terminator (e.g. for \r\n case) */
abstract fun isLineTerminatorPair(char1: Char, char2: Char): Boolean
/** Checks if a [checked] character is after a line terminator using the [previous] character.*/
abstract fun isAfterLineTerminator(previous: Char, checked: Char): Boolean
companion object {
val unixLT: AbstractLineTerminator by lazy {
object : AbstractLineTerminator() {
override fun isLineTerminator(codepoint: Int): Boolean = (codepoint == '\n'.toInt())
override fun isLineTerminatorPair(char1: Char, char2: Char): Boolean = false
override fun isAfterLineTerminator(previous: Char, checked: Char): Boolean = (previous == '\n')
}
}
val unicodeLT: AbstractLineTerminator by lazy {
object : AbstractLineTerminator() {
override fun isLineTerminatorPair(char1: Char, char2: Char): Boolean {
return char1 == '\r' && char2 == '\n'
}
override fun isLineTerminator(codepoint: Int): Boolean {
return codepoint == '\n'.toInt()
|| codepoint == '\r'.toInt()
|| codepoint == '\u0085'.toInt()
|| codepoint or 1 == '\u2029'.toInt()
}
override fun isAfterLineTerminator(previous: Char, checked: Char): Boolean {
return previous == '\n' || previous == '\u0085' || previous.toInt() or 1 == '\u2029'.toInt()
|| previous == '\r' && checked != '\n'
}
}
}
// TODO: Replace with property or another Kotlin-way singleton.
fun getInstance(flag: Int): AbstractLineTerminator {
if (flag and Pattern.UNIX_LINES != 0) {
return unixLT
} else {
return unicodeLT
}
}
}
}
@@ -0,0 +1,547 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* User defined character classes (e.g. [abef]).
*
* @author Nikolay A. Kuznetsov
*/
/*
* TODO: replace the implementation with one using BitSet for first 256 symbols and a hash table / tree for the rest of UTF.
*/
internal class CharClass(val ignoreCase: Boolean = false, negative: Boolean = false) : AbstractCharClass() {
var invertedSurrogates = false
/**
* Shows if the alt flags was inverted during the range construction process.
* E.g. consider the following range: [\D3]. Here we firstly add the \D char class (which has the alt flag set) into a
* resulting char set. After that the resulting char also has the alt flag set. But then we need to add the '3' character
* into this class with positive sense. So we set the inverted flag to show that the range is not negative ([^..])
* by itself but was inverted during some transformations.
*/
var inverted = false
// TODO: May be we can get rid of it?
var hideBits = false
internal var bits_ = BitSet()
override val bits: BitSet?
get() {
if (hideBits)
return null
return bits_
}
var nonBitSet: AbstractCharClass? = null
private val Int.asciiSupplement: Int
get() = when {
this in 'a'.toInt()..'z'.toInt() -> this - 32
this in 'A'.toInt()..'Z'.toInt() -> this + 32
else -> this
}
private val Int.isSurrogate: Boolean
get() = this in Char.MIN_SURROGATE.toInt()..Char.MAX_SURROGATE.toInt()
init {
setNegative(negative)
}
/*
* We can use this method safely even if nonBitSet != null
* due to specific of range constructions in regular expressions.
*/
fun add(ch: Int): CharClass {
var character = ch
if (ignoreCase) {
if (character.toChar() in 'a'..'z' || character.toChar() in 'A'..'Z') {
bits_.set(character.asciiSupplement, !inverted)
} else if (character > 128) {
character = Char.toLowerCase(Char.toUpperCase(character))
}
}
if (character.toChar().isSurrogate()) {
lowHighSurrogates.set(character - Char.MIN_SURROGATE.toInt(), !invertedSurrogates)
}
bits_.set(character, !inverted)
if (!mayContainSupplCodepoints && Char.isSupplementaryCodePoint(ch)) {
mayContainSupplCodepoints = true
}
return this
}
fun add(ch: Char): CharClass = add(ch.toInt())
/*
* The difference between add(AbstractCharClass) and union(AbstractCharClass)
* is that add() is used for constructions like "[^abc\\d]"
* (this pattern doesn't match "1")
* while union is used for constructions like "[^abc[\\d]]"
* (this pattern matches "1").
*/
fun add(another: AbstractCharClass): CharClass {
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
mayContainSupplCodepoints = true
}
// Process surrogates.
if (!invertedSurrogates) {
//A | !B = ! ((A ^ B) & B)
if (another.altSurrogates) {
lowHighSurrogates.xor(another.lowHighSurrogates)
lowHighSurrogates.and(another.lowHighSurrogates)
altSurrogates = !altSurrogates
invertedSurrogates = true
//A | B
} else {
lowHighSurrogates.or(another.lowHighSurrogates)
}
} else {
//!A | !B = !(A & B)
if (another.altSurrogates) {
lowHighSurrogates.and(another.lowHighSurrogates)
//!A | B = !(A & !B)
} else {
lowHighSurrogates.andNot(another.lowHighSurrogates)
}
}
val anotherBits = another.bits
if (!hideBits && anotherBits != null) {
if (!inverted) {
//A | !B = ! ((A ^ B) & B)
if (another.isNegative()) {
bits_.xor(anotherBits)
bits_.and(anotherBits)
alt = !alt
inverted = true
//A | B
} else {
bits_.or(anotherBits)
}
} else {
//!A | !B = !(A & B)
if (another.isNegative()) {
bits_.and(anotherBits)
//!A | B = !(A & !B)
} else {
bits_.andNot(anotherBits)
}
}
// Some of charclasses hides its bits
// Looks like hideBits and nonBitSet are used when we add another class which have no bitmask.
// TODO: We can potentially remove nonBitSet and hideBits. What is alt?
// TODO: The same for or operation.
} else {
val curAlt = alt
if (nonBitSet == null) {
if (curAlt && !inverted && bits_.isEmpty) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return another.contains(ch)
}
}
//alt = true;
} else {
/*
* We keep the value of alt unchanged for
* constructions like [^[abc]fgb] by using
* the formula a ^ b == !a ^ !b.
*/
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(curAlt xor bits_.get(ch) || curAlt xor inverted xor another.contains(ch))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return curAlt xor bits_.get(ch) || curAlt xor inverted xor another.contains(ch)
}
}
//alt = false
}
}
hideBits = true
} else {
val nb = nonBitSet
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(curAlt xor (nb!!.contains(ch) || another.contains(ch)))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return curAlt xor (nb!!.contains(ch) || another.contains(ch))
}
}
//alt = false
}
}
}
return this
}
fun add(start: Int, end: Int): CharClass {
if (start > end)
throw IllegalArgumentException()
//no intersection with surrogate characters
if (!ignoreCase && (end < Char.MIN_SURROGATE.toInt() || start > Char.MAX_SURROGATE.toInt())) {
if (!inverted) {
bits_.set(start, end + 1)
} else {
bits_.clear(start, end + 1)
}
} else {
for (i in start..end) {
add(i)
}
}
return this
}
fun add(start: Char, end: Char): CharClass = add(start.toInt(), end.toInt())
// OR operation
fun union(another: AbstractCharClass) {
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
mayContainSupplCodepoints = true
}
if (altSurrogates xor another.altSurrogates) {
//!A | B = !(A & !B)
if (altSurrogates) {
lowHighSurrogates.andNot(another.lowHighSurrogates)
//A | !B = !((A ^ B) & B)
} else {
lowHighSurrogates.xor(another.lowHighSurrogates)
lowHighSurrogates.and(another.lowHighSurrogates)
altSurrogates = true
}
} else {
//!A | !B = !(A & B)
if (altSurrogates) {
lowHighSurrogates.and(another.lowHighSurrogates)
//A | B
} else {
lowHighSurrogates.or(another.lowHighSurrogates)
}
}
val anotherBits = another.bits
if (!hideBits && anotherBits != null) {
if (alt xor another.isNegative()) {
//!A | B = !(A & !B)
if (alt) {
bits_.andNot(anotherBits)
//A | !B = !((A ^ B) & B)
} else {
bits_.xor(anotherBits)
bits_.and(anotherBits)
alt = true
}
} else {
//!A | !B = !(A & B)
if (alt) {
bits_.and(anotherBits)
//A | B
} else {
bits_.or(anotherBits)
}
}
} else {
val curAlt = alt
if (nonBitSet == null) {
if (!inverted && bits_.isEmpty) {
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !another.contains(ch)
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return another.contains(ch)
}
}
//alt = false
}
} else {
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(another.contains(ch) || curAlt xor bits_.get(ch))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return another.contains(ch) || curAlt xor bits_.get(ch)
}
}
//alt = false
}
}
hideBits = true
} else {
val nb = nonBitSet
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(curAlt xor nb!!.contains(ch) || another.contains(ch))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return curAlt xor nb!!.contains(ch) || another.contains(ch)
}
}
//alt = false
}
}
}
}
// AND operation
fun intersection(another: AbstractCharClass) {
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
mayContainSupplCodepoints = true
}
if (altSurrogates xor another.altSurrogates) {
//!A & B = ((A ^ B) & B)
if (altSurrogates) {
lowHighSurrogates.xor(another.lowHighSurrogates)
lowHighSurrogates.and(another.lowHighSurrogates)
altSurrogates = false
//A & !B
} else {
lowHighSurrogates.andNot(another.lowHighSurrogates)
}
} else {
//!A & !B = !(A | B)
if (altSurrogates) {
lowHighSurrogates.or(another.lowHighSurrogates)
//A & B
} else {
lowHighSurrogates.and(another.lowHighSurrogates)
}
}
val anotherBits = another.bits
if (!hideBits && anotherBits != null) {
if (alt xor another.isNegative()) {
//!A & B = ((A ^ B) & B)
if (alt) {
bits_.xor(anotherBits)
bits_.and(anotherBits)
alt = false
//A & !B
} else {
bits_.andNot(anotherBits)
}
} else {
//!A & !B = !(A | B)
if (alt) {
bits_.or(anotherBits)
//A & B
} else {
bits_.and(anotherBits)
}
}
} else {
val curAlt = alt
if (nonBitSet == null) {
if (!inverted && bits_.isEmpty) {
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !another.contains(ch)
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return another.contains(ch)
}
}
//alt = false
}
} else {
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(another.contains(ch) && curAlt xor bits_.get(ch))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return another.contains(ch) && curAlt xor bits_.get(ch)
}
}
//alt = false
}
}
hideBits = true
} else {
val nb = nonBitSet
if (curAlt) {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return !(curAlt xor nb!!.contains(ch) && another.contains(ch))
}
}
//alt = true
} else {
nonBitSet = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return curAlt xor nb!!.contains(ch) && another.contains(ch)
}
}
//alt = false
}
}
}
}
/**
* Returns `true` if character class contains symbol specified,
* `false` otherwise. Note: #setNegative() method changes the
* meaning of contains method;
* @param ch
* *
* @return `true` if character class contains symbol specified;
* *
*/
override operator fun contains(ch: Int): Boolean {
if (nonBitSet == null) {
return this.alt xor bits_.get(ch) // TODO alt xor bits. It must make sense.
} else {
return alt xor nonBitSet!!.contains(ch)
}
}
override val instance: AbstractCharClass
get() {
if (nonBitSet == null) {
val bs = bits
val res = object : AbstractCharClass() {
override operator fun contains(ch: Int): Boolean {
return this.alt xor bs!!.get(ch)
}
override fun toString(): String {
val temp = StringBuilder()
var i = bs!!.nextSetBit(0)
while (i >= 0) {
temp.append(Char.toChars(i))
temp.append('|')
i = bs.nextSetBit(i + 1)
}
if (temp.length > 0)
temp.deleteCharAt(temp.length - 1)
return temp.toString()
}
}
return res.setNegative(isNegative())
} else {
return this
}
}
//for debugging purposes only
override fun toString(): String {
val temp = StringBuilder()
var i = bits_.nextSetBit(0)
while (i >= 0) {
temp.append(Char.toChars(i))
temp.append('|')
i = bits_.nextSetBit(i + 1)
}
if (temp.length > 0)
temp.deleteCharAt(temp.length - 1)
return temp.toString()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Encapsulates a syntax error that occurred during the compilation of a
* [Pattern]. Might include a detailed description, the original regular
* expression, and the index at which the error occurred.
*
* @author Nikolay A. Kuznetsov
*/
// TODO: Rewrite
class PatternSyntaxException(val description: String = "", val pattern: String = "", index: Int = -1)
: IllegalArgumentException("Error in \"$pattern\" ($index). $description")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,125 @@
/*
* Copyright 2010-2017 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
/**
* Represents a collection of captured groups in a single match of a regular expression.
*
* This collection 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.
*
* An element of the collection at the particular index can be `null`,
* if the corresponding group in the regular expression is optional and
* there was no match captured by that group.
*/
// TODO: Add MatchGroup
interface MatchGroupCollection : Collection<MatchGroup?> {
/** Returns a group with the specified [index].
*
* @return An instance of [MatchGroup] if the group with the specified [index] was matched or `null` otherwise.
*
* Groups are indexed from 1 to the count of groups in the regular expression. A group with the index 0
* corresponds to the entire match.
*/
operator fun get(index: Int): MatchGroup?
}
/**
* Extends [MatchGroupCollection] by introducing a way to get matched groups by name, when regex supports it.
*/
@SinceKotlin("1.1") interface MatchNamedGroupCollection : MatchGroupCollection {
/**
* 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.
*/
operator fun get(name: String): MatchGroup?
}
/**
* Represents the results from a single regular expression match.
*/
interface MatchResult {
/** The range of indices in the original string where match was captured. */
val range: IntRange
/** The substring from the input string captured by this match. */
val value: String
/**
* A collection of groups matched by the regular expression.
*
* This collection 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.
*/
val groups: MatchGroupCollection
/**
* A list of matched indexed group values.
*
* 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 were no match captured by that group,
* corresponding item in [groupValues] is an empty string.
*
* @sample: samples.text.Regexps.matchDestructuringToGroupValues
*/
val groupValues: List<String>
/**
* 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: samples.text.Regexps.matchDestructuring
*/
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).
*/
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.
*
* @sample: samples.text.Regexps.matchDestructuringToGroupValues
*/
class Destructured internal constructor(val match: MatchResult) {
operator inline fun component1(): String = match.groupValues[1]
operator inline fun component2(): String = match.groupValues[2]
operator inline fun component3(): String = match.groupValues[3]
operator inline fun component4(): String = match.groupValues[4]
operator inline fun component5(): String = match.groupValues[5]
operator inline fun component6(): String = match.groupValues[6]
operator inline fun component7(): String = match.groupValues[7]
operator inline fun component8(): String = match.groupValues[8]
operator inline fun component9(): String = match.groupValues[9]
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.
*
* @sample: samples.text.Regexps.matchDestructuringToGroupValues
*/
fun toList(): List<String> = match.groupValues.subList(1, match.groupValues.size)
}
}
@@ -0,0 +1,238 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Match result implementation
* @author Nikolay A. Kuznetsov
*/
// TODO: rework defaults.
// TODO: May be we can remove left/rightBound and just create a substring for matching/searching.
// TODO: We don't use right/leftBound in our API now. So this function can be removed.
// TODO: But I think it may be useful to save them for further improvements of the API.
internal class MatchResultImpl
// TODO: Refactor the constructor comment.
/**
* @param input an input sequence for matching/searching.
* @param regex a [Regex] instance used for matching/searching.
* @param rightBound index in the [input] used as a right bound for matching/searching. Exclusive.
*/
constructor (internal val input: CharSequence,
internal val regex: Regex) : MatchResult {
// Harmony's implementation ========================================================================================
// TODO: Rework number of groups
private val nativePattern = regex.nativePattern
private val groupCount = nativePattern.capturingGroupCount
private val groupBounds = IntArray(groupCount * 2) { -1 }
private val consumers = IntArray(nativePattern.consumersCount + 1) { -1 } // TODO: What is consumers array?
// Used by quantifiers to store a count of a quantified expression occurrences.
val enterCounters: IntArray = IntArray( maxOf(nativePattern.groupQuantifierCount, 0) ) // TODO remove maxOf
var startIndex: Int = 0
set (startIndex: Int) {
field = startIndex
// TODO: what is it? And do we need it?
if (previousMatch < 0) {
previousMatch = startIndex
}
}
var previousMatch = -1
var mode = Regex.Mode.MATCH
// MatchResult interface ===========================================================================================
/** The range of indices in the original string where match was captured. */
override val range: IntRange
get() = getStart(0) until getEnd(0) // TODO: I don't like these get... set... methods. Rename them or make properties.
/** The substring from the input string captured by this match. */
override val value: String
get() = group(0) ?: throw AssertionError("No groupIndex #0 in the match result.")
/**
* A collection of groups matched by the regular expression.
*
* This collection 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.
*/
// Create one object or several ones?
override val groups: MatchGroupCollection = object: MatchGroupCollection, AbstractCollection<MatchGroup?>() {
override val size: Int
get() = this@MatchResultImpl.groupCount
override fun iterator(): Iterator<MatchGroup?> {
return object: Iterator<MatchGroup?> {
var nextIndex: Int = 0
override fun hasNext(): Boolean {
return nextIndex < size
}
override fun next(): MatchGroup? {
if (!hasNext()) {
throw NoSuchElementException()
}
return get(nextIndex++)
}
}
}
override fun get(index: Int): MatchGroup? {
val value = group(index) ?: return null
return MatchGroup(value, getStart(index) until getEnd(index))
}
}
/**
* A list of matched indexed group values.
*
* 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 were no match captured by that group,
* corresponding item in [groupValues] is an empty string.
*
* @sample: samples.text.Regexps.matchDestructuringToGroupValues
*/
override val groupValues: List<String>
get() = mutableListOf<String>().apply {
for (i in 0 until groupCount) {
this.add(group(i) ?: "")
}
}
override fun next(): MatchResult? {
var nextStart = range.endInclusive + 1
if (nextStart == input.length) {
return null
}
// If the current match is empty - shift by 1.
if (nextStart == range.start) {
nextStart++
}
return regex.find(input, nextStart)
}
// =================================================================================================================
// Harmony's implementation ========================================================================================
// TODO: What is it?
fun setConsumed(counter: Int, value: Int) {
this.consumers[counter] = value
}
fun getConsumed(counter: Int): Int {
return this.consumers[counter]
}
fun isCaptured(group: Int): Boolean = getStart(group) >= 0
// Setters and getters for starts and ends of groups ===============================================================
// TODO: Decide what access modifier should we use in this class.
internal fun setStart(group: Int, offset: Int) {
checkGroup(group)
groupBounds[group * 2] = offset
}
internal fun setEnd(group: Int, offset: Int) {
checkGroup(group)
groupBounds[group * 2 + 1] = offset
}
/**
* Returns the index of the first character of the text that matched a given group.
*
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
* @return the character index.
*/
fun getStart(group: Int = 0): Int {
checkGroup(group)
return groupBounds[group * 2]
}
/**
* Returns the index of the first character following the text that matched a given group.
*
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
* @return the character index.
*/
fun getEnd(group: Int = 0): Int {
checkGroup(group)
return groupBounds[group * 2 + 1]
}
// ==================================================================================================
/**
* Returns the text that matched a given group of the regular expression.
*
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
* @return the text that matched the group.
*/
// TODO: Reread and maybe rewrite.
fun group(group: Int = 0): String? {
val start = getStart(group)
val end = getEnd(group)
if (start < 0) { // TODO: Do we need to check end?
return null
}
assert(start <= end && end >= 0 && end <= input.length) // TODO: remove the assert.
return input.subSequence(getStart(group), getEnd(group)).toString()
}
/**
* Returns the number of groups in the result, which is always equal to
* the number of groups in the original regular expression.
*
* @return the number of groups.
*/
fun groupCount(): Int {
return groupCount - 1 // TODO: Do something with it. These +/-1 are terrible.
}
/*
* This method being called after any successful match; For now it's being
* used to check zero group for empty match;
*/
fun finalizeMatch() {
if (this.groupBounds[0] == -1) {
this.groupBounds[0] = this.startIndex
this.groupBounds[1] = this.startIndex
}
previousMatch = getEnd()
}
private fun checkGroup(group: Int) {
if (group < 0 || group > groupCount) {
throw IndexOutOfBoundsException()
}
}
fun updateGroup(index: Int, srtOffset: Int, endOffset: Int) {
checkGroup(index)
groupBounds[index * 2] = srtOffset
groupBounds[index * 2 + 1] = endOffset
}
}
@@ -0,0 +1,868 @@
package kotlin.text
/**
* Apache Harmony Pattern class implementation.
* Analogue for Kotlin Regex class. Need to be merged with it.
*/
/** Represents a compiled pattern used by [Regex] for matching, searching, or replacing strings. */
internal class Pattern(val pattern: String, flags: Int = 0) {
var flags = flags
private set
/** A lexer instance used to get tokens from the pattern. */
private val lexemes = Lexer(pattern, flags)
/* All back references that may be used in pattern. */
private val backRefs = arrayOfNulls<FSet>(BACK_REF_NUMBER)
/** Is true if back referenced sets replacement by second compilation pass is needed.*/
private var needsBackRefReplacement = false
/** Global count of found capturing groups. */
var capturingGroupCount = 0
private set
/** A number of group quantifiers in the pattern */
var groupQuantifierCount = 0
private set
/**
* A number of consumers found in the pattern.
* Consumer is any expression ending with an FSet except capturing groups (they are counted by [capturingGroupCount])
*/
var consumersCount = 0
private set
/** A node to start a matching/searching process by call startNode.matches/startNode.find. */
internal val startNode: AbstractSet
/** Compiles the given pattern */
init {
if (flags != 0 && flags or flagsBitMask != flagsBitMask) {
throw IllegalArgumentException()
}
AbstractSet.counter = 1 // TODO: It's for debug purposes.
startNode = processExpression(-1, this.flags, null)
if (!lexemes.isEmpty()) {
throw PatternSyntaxException()
}
// Finalize compilation
if (needsBackRefReplacement) {
startNode.processSecondPass()
}
}
override fun toString(): String = pattern
/** Return true if the pattern has the specified flag */
private fun hasFlag(flag: Int): Boolean = flags and flag == flag
// Compilation methods. ============================================================================================
/** A->(a|)+ */
private fun processAlternations(last: AbstractSet): AbstractSet {
val auxRange = CharClass(hasFlag(Pattern.CASE_INSENSITIVE))
while (!lexemes.isEmpty() && lexemes.isLetter()
&& (lexemes.lookAhead == 0
|| lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_PARENTHESIS)) {
auxRange.add(lexemes.next())
if (lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR) {
lexemes.next()
}
}
val rangeSet = processRangeSet(auxRange)
rangeSet.next = last
return rangeSet
}
/** E->AE; E->S|E; E->S; A->(a|)+ E->S(|S)* */
private fun processExpression(ch: Int, newFlags: Int, last: AbstractSet?): AbstractSet {
val children = ArrayList<AbstractSet>()
val savedFlags = flags
var saveChangedFlags = false
if (newFlags != flags) {
flags = newFlags
}
// Create a right finalizing set.
val fSet: FSet
when (ch) {
// Special groups: non-capturing, look ahead/behind etc.
Lexer.CHAR_NONCAP_GROUP -> fSet = NonCapFSet(consumersCount++)
Lexer.CHAR_POS_LOOKAHEAD,
Lexer.CHAR_NEG_LOOKAHEAD -> fSet = AheadFSet()
Lexer.CHAR_POS_LOOKBEHIND,
Lexer.CHAR_NEG_LOOKBEHIND -> fSet = BehindFSet(consumersCount++)
Lexer.CHAR_ATOMIC_GROUP -> fSet = AtomicFSet(consumersCount++)
// A Capturing group.
else -> {
if (last == null) {
// Whole pattern - group #0.
fSet = FinalSet()
saveChangedFlags = true
} else {
// TODO: Looks like next here is null. Or it is set later.
fSet = FSet(capturingGroupCount)
}
if (capturingGroupCount < BACK_REF_NUMBER) {
backRefs[capturingGroupCount] = fSet
}
capturingGroupCount++
}
}
//Process to EOF or ')'
do {
val child: AbstractSet
when {
// a|...
lexemes.isLetter() && lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR -> child = processAlternations(fSet)
// ..|.., e.g. in "a||||b"
lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR -> {
child = EmptySet(fSet)
lexemes.next()
}
else -> {
child = processSubExpression(fSet)
if (lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR) {
lexemes.next()
}
}
}
// TODO: Do we realy need this null check? Looks like child cannot be null.
children.add(child)
} while (!(lexemes.isEmpty() || lexemes.currentChar == Lexer.CHAR_RIGHT_PARENTHESIS))
// |) or |<EOF> - add an empty node.
if (lexemes.lookBack == Lexer.CHAR_VERTICAL_BAR) {
children.add(EmptySet(fSet))
}
// Restore flags.
if (flags != savedFlags && !saveChangedFlags) {
flags = savedFlags
lexemes.restoreFlags(flags)
}
when (ch) {
Lexer.CHAR_NONCAP_GROUP -> return NonCapturingJointSet(children, fSet)
Lexer.CHAR_POS_LOOKAHEAD -> return PositiveLookAheadSet(children, fSet)
Lexer.CHAR_NEG_LOOKAHEAD -> return NegativeLookAheadSet(children, fSet)
Lexer.CHAR_POS_LOOKBEHIND -> return PositiveLookBehindSet(children, fSet)
Lexer.CHAR_NEG_LOOKBEHIND -> return NegativeLookBehindSet(children, fSet)
Lexer.CHAR_ATOMIC_GROUP -> return AtomicJointSet(children, fSet)
else -> when (children.size) {
0 -> return EmptySet(fSet)
1 -> return SingleSet(children[0], fSet)
else -> return JointSet(children, fSet)
}
}
}
/**
* T->aaa
*/
private fun processSequence(): AbstractSet {
val substring = StringBuilder()
while (!lexemes.isEmpty()
&& lexemes.isLetter()
&& !lexemes.isSurrogate()
&& (!lexemes.isNextSpecial && lexemes.lookAhead == 0 // End of a pattern.
|| !lexemes.isNextSpecial && Lexer.isLetter(lexemes.lookAhead)
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_PARENTHESIS
|| lexemes.lookAhead and 0x8000ffff.toInt() == Lexer.CHAR_LEFT_PARENTHESIS
|| lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR
|| lexemes.lookAhead == Lexer.CHAR_DOLLAR)) {
val ch = lexemes.next()
// TODO: Adapt to our checks.
if (Char.isSupplementaryCodePoint(ch)) {
substring.append(Char.toChars(ch))
} else {
substring.append(ch.toChar())
}
}
return SequenceSet(substring, hasFlag(CASE_INSENSITIVE))
}
/**
* D->a
*/
// TODO: Refactor
private fun processDecomposedChar(): AbstractSet {
val codePoints = IntArray(Lexer.MAX_DECOMPOSITION_LENGTH)
val codePointsHangul: CharArray
var readCodePoints = 0
var curSymb = -1
var curSymbIndex = -1
if (!lexemes.isEmpty() && lexemes.isLetter()) {
curSymb = lexemes.next()
codePoints[readCodePoints] = curSymb
curSymbIndex = curSymb - Lexer.LBase
}
/*
* We process decomposed Hangul syllable LV or LVT or process jamo L.
* See http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf
* "3.12 Conjoining Jamo Behavior"
*/
if (curSymbIndex >= 0 && curSymbIndex < Lexer.LCount) {
codePointsHangul = CharArray(Lexer.MAX_HANGUL_DECOMPOSITION_LENGTH)
codePointsHangul[readCodePoints++] = curSymb.toChar()
curSymb = lexemes.currentChar
curSymbIndex = curSymb - Lexer.VBase
if (curSymbIndex >= 0 && curSymbIndex < Lexer.VCount) {
codePointsHangul[readCodePoints++] = curSymb.toChar()
lexemes.next()
curSymb = lexemes.currentChar
curSymbIndex = curSymb - Lexer.TBase
if (curSymbIndex >= 0 && curSymbIndex < Lexer.TCount) {
codePointsHangul[readCodePoints++] = curSymb.toChar()
lexemes.next()
//LVT syllable
return HangulDecomposedCharSet(codePointsHangul, 3)
} else {
//LV syllable
return HangulDecomposedCharSet(codePointsHangul, 2)
}
} else {
//L jamo
return CharSet(codePointsHangul[0], hasFlag(CASE_INSENSITIVE))
}
/*
* We process single codepoint or decomposed codepoint.
* We collect decomposed codepoint and obtain
* one DecomposedCharSet.
*/
} else {
readCodePoints++
while (readCodePoints < Lexer.MAX_DECOMPOSITION_LENGTH
&& !lexemes.isEmpty() && lexemes.isLetter()
&& !Lexer.isDecomposedCharBoundary(lexemes.currentChar)) {
codePoints[readCodePoints++] = lexemes.next()
}
/*
* We have read an ordinary symbol.
*/
if (readCodePoints == 1 && !Lexer.hasSingleCodepointDecomposition(codePoints[0])) {
return processCharSet(codePoints[0])
} else {
return DecomposedCharSet(codePoints, readCodePoints)
}
}
}
/**
* S->BS; S->QS; S->Q; B->a+
*/
private fun processSubExpression(last: AbstractSet): AbstractSet {
var cur: AbstractSet
when {
lexemes.isLetter() && !lexemes.isNextSpecial && Lexer.isLetter(lexemes.lookAhead) -> {
when {
hasFlag(Pattern.CANON_EQ) -> {
cur = processDecomposedChar()
if (!lexemes.isEmpty()
&& (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS || last is FinalSet)
&& lexemes.currentChar != Lexer.CHAR_VERTICAL_BAR
&& !lexemes.isLetter()) {
cur = processQuantifier(last, cur)
}
}
lexemes.isHighSurrogate() || lexemes.isLowSurrogate() -> {
val term = processTerminal(last) // TODO: term is not null.
cur = processQuantifier(last, term)
}
else -> {
cur = processSequence()
}
}
}
lexemes.currentChar == Lexer.CHAR_RIGHT_PARENTHESIS -> {
if (last is FinalSet) {
throw PatternSyntaxException()
}
cur = EmptySet(last)
}
else -> {
val term = processTerminal(last)
cur = processQuantifier(last, term)
}
}
if (!lexemes.isEmpty()
&& (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS || last is FinalSet)
&& lexemes.currentChar != Lexer.CHAR_VERTICAL_BAR) {
val next = processSubExpression(last)
if (cur is LeafQuantifierSet
// '*' or '{0,}' quantifier
&& cur.max == Quantifier.INF
&& cur.min == 0
&& !next.first(cur.innerSet)) {
// An Optimizer node for the case where there is no intersection with the next node
cur = UnifiedQuantifierSet(cur)
}
cur.next = next
} else {
cur.next = last
}
return cur
}
/**
* Q->T(*|+|?...) also do some optimizations.
*/
private fun processQuantifier(last: AbstractSet, term: AbstractSet): AbstractSet {
val quant = lexemes.currentChar
if (term !is LeafSet) {
return when (quant) {
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
val q: QuantifierSet
lexemes.next()
if (term.type == AbstractSet.TYPE_DOTSET) {
q = DotQuantifierSet(term, last, quant, AbstractLineTerminator.getInstance(flags), hasFlag(Pattern.DOTALL))
} else {
q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
}
term.next = q
q
}
Lexer.QUANT_ALT -> {
lexemes.next()
val q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R, Lexer.QUANT_ALT_R -> {
lexemes.next()
val q = ReluctantGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P, Lexer.QUANT_ALT_P -> {
lexemes.next()
PossessiveGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
}
Lexer.QUANT_COMP -> {
val q = GroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_COMP_R -> {
val q = ReluctantGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
term.next = q
q
}
Lexer.QUANT_COMP_P -> {
return PossessiveGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
}
else -> term
}
} else {
val leaf: LeafSet = term
return when (quant) {
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
lexemes.next()
val q = LeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
}
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R -> {
lexemes.next()
val q = ReluctantLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
}
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P -> {
lexemes.next()
val q = PossessiveLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
leaf.next = q
q
}
Lexer.QUANT_ALT -> {
lexemes.next()
LeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT)
}
Lexer.QUANT_ALT_R -> {
lexemes.next()
ReluctantLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_R)
}
Lexer.QUANT_ALT_P -> {
lexemes.next()
PossessiveLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_P)
}
Lexer.QUANT_COMP -> {
LeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP)
}
Lexer.QUANT_COMP_R -> {
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_R)
}
Lexer.QUANT_COMP_P -> {
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_P)
}
else -> term
}
}
}
/**
* T-> letter|[range]|{char-class}|(E)
*/
private fun processTerminal(last: AbstractSet): AbstractSet {
val term: AbstractSet
var char = lexemes.currentChar
// Process flags: (?...)(?...)...
while (char and 0xff00ffff.toInt() == Lexer.CHAR_FLAGS) {
lexemes.next()
flags = (char shr 16) and flagsBitMask
char = lexemes.currentChar
}
// The terminal is some kind of group: (E). Call processExpression for it.
if (char and 0x8000ffff.toInt() == Lexer.CHAR_LEFT_PARENTHESIS) {
lexemes.next()
var newFlags = flags
if (char and 0xff00ffff.toInt() == Lexer.CHAR_NONCAP_GROUP) {
newFlags = (char shr 16) and flagsBitMask
}
term = processExpression(char and 0xff00ffff.toInt(), newFlags, last) // Remove flags from the token.
if (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS) {
throw PatternSyntaxException()
}
lexemes.next()
} else {
// Other terminals.
when (char) {
Lexer.CHAR_LEFT_SQUARE_BRACKET -> { // Range: [...]
lexemes.next()
var negative = false
if (lexemes.currentChar == Lexer.CHAR_CARET) {
negative = true
lexemes.next()
}
term = processRange(negative, last)
if (lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET) {
throw PatternSyntaxException()
}
// TODO: Set mode lexer as Mode.Range is set.
lexemes.setModeWithReread(Lexer.Mode.PATTERN)
lexemes.next()
}
Lexer.CHAR_DOT -> { // Dot: .
lexemes.next()
term = DotSet(AbstractLineTerminator.getInstance(flags), hasFlag(DOTALL))
}
Lexer.CHAR_CARET -> { // Beginning of the string: ^
lexemes.next()
term = SOLSet(AbstractLineTerminator.getInstance(flags), hasFlag(MULTILINE))
consumersCount++
}
Lexer.CHAR_DOLLAR -> { // End of the string: $
lexemes.next()
term = EOLSet(consumersCount++, AbstractLineTerminator.getInstance(flags), hasFlag(MULTILINE))
}
// Word / non-word boundary.
Lexer.CHAR_WORD_BOUND -> {
lexemes.next()
term = WordBoundarySet(true)
}
Lexer.CHAR_NONWORD_BOUND -> {
lexemes.next()
term = WordBoundarySet(false)
}
Lexer.CHAR_END_OF_INPUT -> { // End of an input: \z
lexemes.next()
term = EOISet()
}
Lexer.CHAR_END_OF_LINE -> { // End of a line: \Z
lexemes.next()
term = EOLSet(consumersCount++, AbstractLineTerminator.getInstance(flags))
}
Lexer.CHAR_START_OF_INPUT -> { // Start if an input: \A
lexemes.next()
term = SOLSet(AbstractLineTerminator.getInstance(flags))
}
Lexer.CHAR_PREVIOUS_MATCH -> { // A previous match: \G
lexemes.next()
term = PreviousMatchSet()
}
// Back references: \1, \2 etc.
0x80000000.toInt() or '1'.toInt(),
0x80000000.toInt() or '2'.toInt(),
0x80000000.toInt() or '3'.toInt(),
0x80000000.toInt() or '4'.toInt(),
0x80000000.toInt() or '5'.toInt(),
0x80000000.toInt() or '6'.toInt(),
0x80000000.toInt() or '7'.toInt(),
0x80000000.toInt() or '8'.toInt(),
0x80000000.toInt() or '9'.toInt() -> {
val number = (char and 0x7FFFFFFF) - '0'.toInt()
if (number < capturingGroupCount) { // All is ok - the group exists.
lexemes.next()
term = BackReferenceSet(number, consumersCount++, hasFlag(CASE_INSENSITIVE))
// backRefs[number] is proved to be not null because the group is already created (number < capturingGroupCount)
backRefs[number]!!.isBackReferenced = true
needsBackRefReplacement = true // And process back references in the second pass.
} else {
throw PatternSyntaxException() // Wrong group number.
}
}
// A special token (\D, \w etc), 'u0000' or the end of the pattern.
0 -> {
val cc: AbstractCharClass? = lexemes.curSpecialToken as AbstractCharClass?
when {
cc != null -> {
term = processRangeSet(cc)
lexemes.next()
}
!lexemes.isEmpty() -> {
term = CharSet(char.toChar())
lexemes.next()
}
else -> term = EmptySet(last)
}
}
else -> {
when {
// A regular character.
char >= 0 && !lexemes.isSpecial -> {
term = processCharSet(char)
lexemes.next()
}
char == Lexer.CHAR_VERTICAL_BAR -> {
term = EmptySet(last)
}
char == Lexer.CHAR_RIGHT_PARENTHESIS -> {
if (last is FinalSet) {
throw PatternSyntaxException()
}
term = EmptySet(last)
}
else -> throw PatternSyntaxException()
}
}
}
}
return term
}
/**
* Process [...] ranges
*/
// TODO: We can merge it with processRangeExpression.
private fun processRange(negative: Boolean, last: AbstractSet): AbstractSet {
val res = processRangeExpression(negative)
val rangeSet = processRangeSet(res)
rangeSet.next = last
return rangeSet
}
private fun processRangeExpression(alt: Boolean): CharClass {
var result = CharClass(hasFlag(Pattern.CASE_INSENSITIVE), alt)
var buffer = -1
var intersection = false
var firstInClass = true
var notClosed = lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET
while (!lexemes.isEmpty() && (notClosed || firstInClass)) {
when (lexemes.currentChar) {
Lexer.CHAR_RIGHT_SQUARE_BRACKET -> {
if (buffer >= 0) {
result.add(buffer)
}
buffer = ']'.toInt()
lexemes.next()
}
Lexer.CHAR_LEFT_SQUARE_BRACKET -> {
if (buffer >= 0) {
result.add(buffer)
buffer = -1
}
lexemes.next()
var negative = false
if (lexemes.currentChar == Lexer.CHAR_CARET) {
lexemes.next()
negative = true
}
if (intersection)
result.intersection(processRangeExpression(negative))
else
result.union(processRangeExpression(negative))
intersection = false
lexemes.next()
}
Lexer.CHAR_AMPERSAND -> {
if (buffer >= 0) {
result.add(buffer)
}
buffer = lexemes.next() // buffer == Lexer.CHAR_AMPERSAND since next() returns currentChar.
/*
* If there is a start for subrange we will do an intersection
* otherwise treat '&' as a normal character
*/
if (lexemes.currentChar == Lexer.CHAR_AMPERSAND) {
if (lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET) {
lexemes.next()
intersection = true
buffer = -1
} else {
lexemes.next()
if (firstInClass) {
// Skip "&&" at "[&&...]" or "[^&&...]"
result = processRangeExpression(false)
} else {
// Ignore "&&" at "[X&&]" ending where X != empty string
if (lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET) {
result.intersection(processRangeExpression(false))
}
}
}
} else {
//treat '&' as a normal character
buffer = '&'.toInt()
}
}
Lexer.CHAR_HYPHEN -> {
if (firstInClass
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_SQUARE_BRACKET
|| lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET
|| buffer < 0) {
// Treat the hypen as a normal character.
if (buffer >= 0) {
result.add(buffer)
}
buffer = '-'.toInt()
lexemes.next()
} else {
// A range.
lexemes.next()
var cur = lexemes.currentChar
if (!lexemes.isSpecial
&& (cur >= 0
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_SQUARE_BRACKET
|| lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET
|| buffer < 0)) {
try {
if (!Lexer.isLetter(cur)) {
cur = cur and 0xFFFF
}
result.add(buffer, cur)
} catch (e: Exception) {
throw PatternSyntaxException()
}
lexemes.next()
buffer = -1
} else {
throw PatternSyntaxException()
}
}
}
Lexer.CHAR_CARET -> {
if (buffer >= 0) {
result.add(buffer)
}
buffer = '^'.toInt()
lexemes.next()
}
0 -> {
if (buffer >= 0) {
result.add(buffer)
}
val cs = lexemes.curSpecialToken as AbstractCharClass?
if (cs != null) {
result.add(cs)
buffer = -1
} else {
buffer = 0
}
lexemes.next()
}
else -> {
if (buffer >= 0) {
result.add(buffer)
}
buffer = lexemes.next()
}
}
firstInClass = false
notClosed = lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET
}
if (notClosed) {
throw PatternSyntaxException()
}
if (buffer >= 0) {
result.add(buffer)
}
return result
}
// TODO: Reread
private fun processRangeSet(charClass: AbstractCharClass): AbstractSet {
if (charClass.hasLowHighSurrogates()) {
val lowHighSurrRangeSet = SurrogateRangeSet(charClass.surrogates)
if (charClass.mayContainSupplCodepoints) {
return CompositeRangeSet(SupplementaryRangeSet(charClass.withoutSurrogates, hasFlag(CASE_INSENSITIVE)), lowHighSurrRangeSet)
}
return CompositeRangeSet(RangeSet(charClass.withoutSurrogates, hasFlag(CASE_INSENSITIVE)), lowHighSurrRangeSet)
}
if (charClass.mayContainSupplCodepoints) {
return SupplementaryRangeSet(charClass, hasFlag(CASE_INSENSITIVE))
}
return RangeSet(charClass, hasFlag(CASE_INSENSITIVE))
}
private fun processCharSet(ch: Int): AbstractSet {
val isSupplCodePoint = Char.isSupplementaryCodePoint(ch)
return when {
isSupplCodePoint -> SequenceSet(fromCharArray(Char.toChars(ch), 0, 2), hasFlag(CASE_INSENSITIVE))
ch.toChar().isLowSurrogate() -> LowSurrogateCharSet(ch.toChar())
ch.toChar().isHighSurrogate() -> HighSurrogateCharSet(ch.toChar())
else -> CharSet(ch.toChar(), hasFlag(CASE_INSENSITIVE))
}
}
companion object {
//TODO: Use RegexOption enum here.
// Flags.
/**
* This constant specifies that a pattern matches Unix line endings ('\n')
* only against the '.', '^', and '$' meta characters.
*/
val UNIX_LINES = 1 shl 0
/**
* This constant specifies that a `Pattern` is matched
* case-insensitively. That is, the patterns "a+" and "A+" would both match
* the string "aAaAaA".
*/
val CASE_INSENSITIVE = 1 shl 1
/**
* This constant specifies that a `Pattern` may contain whitespace or
* comments. Otherwise comments and whitespace are taken as literal
* characters.
*/
val COMMENTS = 1 shl 2
/**
* This constant specifies that the meta characters '^' and '$' match only
* the beginning and end end of an input line, respectively. Normally, they
* match the beginning and the end of the complete input.
*/
val MULTILINE = 1 shl 3
/**
* This constant specifies that the whole `Pattern` is to be taken
* literally, that is, all meta characters lose their meanings.
*/
val LITERAL = 1 shl 4
/**
* This constant specifies that the '.' meta character matches arbitrary
* characters, including line endings, which is normally not the case.
*/
val DOTALL = 1 shl 5
/**
* This constant specifies that a character in a `Pattern` and a
* character in the input string only match if they are canonically
* equivalent.
*/
val CANON_EQ = 1 shl 6
/** Max number of back references supported. */
internal val BACK_REF_NUMBER = 10
/** A bit mask that includes all defined match flags */
internal val flagsBitMask = Pattern.UNIX_LINES or
Pattern.CASE_INSENSITIVE or
Pattern.COMMENTS or
Pattern.MULTILINE or
Pattern.LITERAL or
Pattern.DOTALL or
Pattern.CANON_EQ
/**
* Quotes a given string using "\Q" and "\E", so that all other meta-characters lose their special meaning.
* If the string is used for a `Pattern` afterwards, it can only be matched literally.
*/
// TODO: Replace with other functions.
fun quote(s: String): String {
return StringBuilder()
.append("\\Q")
.append(s.replace("\\E", "\\E\\\\E\\Q"))
.append("\\E").toString()
}
}
}
@@ -0,0 +1,41 @@
// TODO: License.
package kotlin.text
import kotlin.IllegalArgumentException
/**
* Represents RE quantifier; contains two fields responsible for min and max number of repetitions.
* Negative value for maximum number of repetition represents infinity(i.e. +,*)
* @author Nikolay A. Kuznetsov
*/
// TODO: May be replace with some other class (Range?).
internal class Quantifier(val min: Int, val max: Int = min) : SpecialToken() {
init {
if (min < 0 || max < -1) {
throw IllegalArgumentException()
}
}
override fun toString() = "{$min, ${if (max == -1) "" else max}}"
override val type: Type = SpecialToken.Type.QUANTIFIER
companion object {
val starQuantifier = Quantifier(0, -1)
val plusQuantifier = Quantifier(1, -1)
val altQuantifier = Quantifier(0, 1)
val INF = -1
fun fromLexerToken(token: Int) = when(token) {
Lexer.QUANT_STAR, Lexer.QUANT_STAR_P, Lexer.QUANT_STAR_R -> starQuantifier
Lexer.QUANT_ALT, Lexer.QUANT_ALT_P, Lexer.QUANT_ALT_R -> altQuantifier
Lexer.QUANT_PLUS, Lexer.QUANT_PLUS_P, Lexer.QUANT_PLUS_R -> plusQuantifier
else -> throw IllegalArgumentException()
}
}
}
@@ -0,0 +1,322 @@
/*
* Copyright 2010-2017 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
private interface FlagEnum {
val value: Int
val mask: Int
}
private fun Iterable<FlagEnum>.toInt(): Int = this.fold(0, { value, option -> value or option.value })
private fun fromInt(value: Int): Set<RegexOption> =
RegexOption.values().filterTo(mutableSetOf<RegexOption>()) { value and it.mask == it.value }
/**
* Provides enumeration values to use to set regular expression options.
*/
enum class RegexOption(override val value: Int, override val mask: Int = value) : FlagEnum {
// common
/** Enables case-insensitive matching. Case comparison is Unicode-aware. */
IGNORE_CASE(Pattern.CASE_INSENSITIVE),
/**
* Enables multiline mode.
* In multiline mode the expressions `^` and `$` match just after or just before,
* respectively, a line terminator or the end of the input sequence.
*/
MULTILINE(Pattern.MULTILINE),
//jvm-specific
/**
* Enables literal parsing of the pattern.
* Metacharacters or escape sequences in the input sequence will be given no special meaning.
*/
LITERAL(Pattern.LITERAL),
/**
* Enables Unix lines mode.
* In this mode, only the `'\n'` is recognized as a line terminator.
*/
UNIX_LINES(Pattern.UNIX_LINES),
/** Permits whitespace and comments in pattern. */
COMMENTS(Pattern.COMMENTS),
/** Enables the mode, when the expression `.` matches any character,
* including a line terminator.
*/
DOT_MATCHES_ALL(Pattern.DOTALL),
/** Enables equivalence by canonical decomposition. */
CANON_EQ(Pattern.CANON_EQ)
}
/**
* Represents the results from a single capturing group within a [MatchResult] of [Regex].
*
* @param value The value of captured group.
* @param range The range of indices in the input string where group was captured.
*
* The [range] property is available on JVM only.
*/
data class MatchGroup(val value: String, val range: IntRange)
/**
* Represents an immutable regular expression.
*
* For pattern syntax reference see [Pattern]
*/
class Regex internal constructor(internal val nativePattern: Pattern) {
enum class Mode {
FIND, MATCH
}
/** Creates a regular expression from the specified [pattern] string and the default options. */
constructor(pattern: String): this(Pattern(pattern))
/** Creates a regular expression from the specified [pattern] string and the specified single [option]. */
constructor(pattern: String, option: RegexOption): this(Pattern(pattern, ensureUnicodeCase(option.value)))
/** Creates a regular expression from the specified [pattern] string and the specified set of [options]. */
constructor(pattern: String, options: Set<RegexOption>): this(Pattern(pattern, ensureUnicodeCase(options.toInt())))
/** The pattern string of this regular expression. */
val pattern: String
get() = nativePattern.pattern
private val startNode = nativePattern.startNode
/** The set of options that were used to create this regular expression. */
val options: Set<RegexOption> = fromInt(nativePattern.flags)
companion object {
/** Returns a literal regex for the specified [literal] string. */
// TODO: Uncomment for native
fun fromLiteral(literal: String): Regex = Regex(literal, RegexOption.LITERAL)
/** Returns a literal pattern for the specified [literal] string. */
fun escape(literal: String): String = Pattern.quote(literal)
/**
* Returns a replacement string for the given one that has all backslashes
* and dollar signs escaped.
*/
fun escapeReplacement(literal: String): String {
if (!literal.contains('\\') && !literal.contains('$'))
return literal
val result = StringBuilder(literal.length * 2)
literal.forEach {
if (it == '\\' || it == '$') {
result.append('\\')
}
result.append(it)
}
return result.toString()
}
// TODO: Remove
private fun ensureUnicodeCase(flags: Int) = flags
}
private fun doMatch(input: CharSequence, mode: Mode): MatchResult? {
// TODO: Harmony has a default constructor for MatchResult. Do we need it?
// TODO: Reuse the matchResult.
val matchResult = MatchResultImpl(input, this)
matchResult.mode = mode
val matches = startNode.matches(0, input, matchResult) >= 0
if (!matches) {
return null
}
matchResult.finalizeMatch()
return matchResult
}
/** Indicates whether the regular expression matches the entire [input]. */
infix fun matches(input: CharSequence): Boolean = doMatch(input, Mode.MATCH) != null
/** Indicates whether the regular expression can find at least one match in the specified [input]. */
// TODO: Looks like we don't need Mode anymore.
fun containsMatchIn(input: CharSequence): Boolean = find(input) != null
/**
* Returns the first match of a regular expression in the [input], beginning at the specified [startIndex].
*
* @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()`
* @return An instance of [MatchResult] if match was found or `null` otherwise.
*/
fun find(input: CharSequence, startIndex: Int = 0): MatchResult? {
if (startIndex < 0 || startIndex > input.length) {
throw IndexOutOfBoundsException() // TODO: Add a message.
}
// TODO: reuse the match result?
val matchResult = MatchResultImpl(input, this)
matchResult.mode = Mode.FIND
matchResult.startIndex = startIndex
val foundIndex = startNode.find(startIndex, input, matchResult)
if (foundIndex >= 0) {
matchResult.finalizeMatch()
return matchResult
} else {
/*matchResult.hitEnd = true
matchResult.startIndex = -1*/
return null
}
}
/**
* Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex].
*/
fun findAll(input: CharSequence, startIndex: Int = 0): Sequence<MatchResult>
= generateSequence({ find(input, startIndex) }, MatchResult::next)
/**
* Attempts to match the entire [input] CharSequence against the pattern.
*
* @return An instance of [MatchResult] if the entire input matches or `null` otherwise.
*/
fun matchEntire(input: CharSequence): MatchResult?= doMatch(input, Mode.MATCH)
private fun processReplacement(match: MatchResult, replacement: String): String {
val result = StringBuilder(replacement.length)
var escaped = false
var backReference = false
for (ch in replacement) {
when {
escaped -> {
result.append(ch)
escaped = false
}
backReference -> {
if (ch !in '0'..'9') {
throw IllegalArgumentException("Incorrect back reference: $ch.")
}
val group = ch - '0'
result.append(match.groupValues[group])
// We don't catch IndexOutOfBoundException here because
// it's a correct exception in case of a wrong group number.
// TODO: But we can rethrow it with more informative message.
backReference = false
}
ch == '\\' -> escaped = true
ch == '$' -> backReference = true
else -> result.append(ch)
}
}
if (backReference || escaped) {
throw IllegalArgumentException("Unexpected end of replacement.")
}
return result.toString()
}
/**
* Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression.
*
* @param replacement A replacement expression that can include substitutions. See [Matcher.appendReplacement] for details.
*/
fun replace(input: CharSequence, replacement: String): String
= replace(input) { match -> processReplacement(match, replacement) }
/**
* Replaces all occurrences of this regular expression in the specified [input] string with the result of
* the given function [transform] that takes [MatchResult] and returns a string to be used as a
* replacement for that match.
*/
fun replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String {
var match: MatchResult? = find(input) ?: return input.toString()
var lastStart = 0
val length = input.length
val sb = StringBuilder(length)
do {
val foundMatch = match!!
sb.append(input, lastStart, foundMatch.range.start)
sb.append(transform(foundMatch))
lastStart = foundMatch.range.endInclusive + 1
match = foundMatch.next()
} while (lastStart < length && match != null)
if (lastStart < length) {
sb.append(input, lastStart, length)
}
return sb.toString()
}
/**
* Replaces the first occurrence of this regular expression in the specified [input] string with specified [replacement] expression.
*
* @param replacement A replacement expression that can include substitutions. See [Matcher.appendReplacement] for details.
*/
fun replaceFirst(input: CharSequence, replacement: String): String
= replaceFirst(input) { match -> processReplacement(match, replacement) }
/**
* Splits the [input] CharSequence around matches of this regular expression.
*
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
* Zero by default means no limit is set.
*/
// TODO: replace all argument checks with require function.
fun split(input: CharSequence, limit: Int = 0): List<String> {
require(limit >= 0, { "Limit must be non-negative, but was $limit." } )
if (input.isEmpty()) {
return listOf("")
} else {
var lastStart = 0
val result = mutableListOf<String>()
var match: MatchResult? = find(input)
while (match != null && (limit == 0 || result.size < limit - 1)) {
result.add(input.substring(lastStart, match.range.start))
lastStart = match.range.endInclusive + 1
match = match.next()
}
result.add(input.substring(lastStart, input.length))
return result
}
}
/** Returns the string representation of this regular expression, namely the [pattern] of this regular expression. */
override fun toString(): String = nativePattern.toString()
// Native specific =================================================================================================
fun lookingAt(input: CharSequence): Boolean = doMatch(input, Mode.FIND) != null
/** Indicates whether the regular expression can find at least one match in the specified [input] starting with [index]. */
fun containsMatchIn(input: CharSequence, index: Int): Boolean = find(input, index) != null
/** Replaces the first occurrence of this regular expression in the specified [input] string with specified using specified transfromation */
fun replaceFirst(input: CharSequence, transform: (MatchResult) -> CharSequence): String {
val match = find(input) ?: return input.toString()
val length = input.length
val result = StringBuilder(length)
result.append(input, 0, match.range.start)
result.append(transform(match))
if (match.range.endInclusive + 1 < length) {
result.append(input, match.range.endInclusive + 1, length)
}
return result.toString()
}
}
@@ -0,0 +1,161 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 kotlin.AssertionError
/** Basic class for sets which have no complex next node handling. */
internal abstract class SimpleSet : AbstractSet {
override var next: AbstractSet = dummyNext
constructor()
constructor(type : Int): super(type)
}
/**
* Basic class for nodes, representing given regular expression.
* Note: (Almost) All the classes representing nodes has 'set' suffix.
*
* @author Nikolay A. Kuznetsov
*/
internal abstract class AbstractSet(val type: Int = 0) {
companion object {
const val TYPE_LEAF = 1 shl 0
const val TYPE_FSET = 1 shl 1
const val TYPE_QUANT = 1 shl 3
const val TYPE_DOTSET = 0x80000000.toInt() or '.'.toInt()
/** Counter for debugging purposes, represent unique node index. */
var counter = 1
val dummyNext = object : AbstractSet() {
override var next: AbstractSet
get() = throw AssertionError("This method is not expected to be called.")
set(value) {}
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl) =
throw AssertionError("This method is not expected to be called.")
override fun hasConsumed(matchResult: MatchResultImpl): Boolean =
throw AssertionError("This method is not expected to be called.")
override fun processSecondPassInternal(): AbstractSet = this
}
}
var secondPassVisited = false
abstract var next: AbstractSet
// These properties and toString() method are for debug purposes.
protected var debugIndex = AbstractSet.counter++.toString()
protected open val name: String
get() = ""
override fun toString(): String = "<$debugIndex:$name>"
/**
* Checks if this node matches in given position and recursively call
* next node matches on positive self match. Returns positive integer if
* entire match succeed, negative otherwise.
* @param startIndex - string index to start from.
* @param testString - input string.
* @param matchResult - MatchResult to sore result into.
* @return -1 if match fails or n > 0;
*/
abstract fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
/**
* Attempts to apply pattern starting from this set/startIndex; returns
* index this search was started from, if value is negative, this means that
* this search didn't succeed, additional information could be obtained via
* matchResult.
*
* Note: this is default implementation for find method, it's based on
* matches, subclasses do not have to override find method unless
* more effective find method exists for a particular node type
* (sequence, i.e. substring, for example). Same applies for find back
* method.
*
* @param startIndex - starting index.
* @param testString - string to search in.
* @param matchResult - result of the match.
* @return last searched index.
*/
open fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
= (startIndex..testString.length).firstOrNull { index -> matches(index, testString, matchResult) >= 0 }
?: -1
/**
* @param leftLimit - an index, to finish search back (left limit, inclusive).
* @param rightLimit - an index to start search from (right limit, exclusive). // TODO: Is it really exclusive
* @param testString - test string.
* @param matchResult - match result.
* @return an index to start back search next time if this search fails(new left bound);
* if this search fails the value is negative.
*/
open fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
= (rightLimit downTo leftLimit).firstOrNull { index -> matches(index, testString, matchResult) >= 0 }
?: -1
/**
* Returns true, if this node has consumed any characters during
* positive match attempt, for example node representing character always
* consumes one character if it matches. If particular node matches
* empty sting this method will return false.
*
* @param matchResult - match result;
* @return true if the node consumes any character and false otherwise.
*/
abstract fun hasConsumed(matchResult: MatchResultImpl): Boolean
/**
* Returns true if the given node intersects with this one, false otherwise.
* This method is being used for quantifiers construction, lets consider the
* following regular expression (a|b)*ccc. (a|b) does not intersects with "ccc"
* and thus can be quantified greedily (w/o kickbacks), like *+ instead of *.
* @param set - A node the intersection is checked for. Usually a previous node.
* @return true if the given node intersects with this one, false otherwise.
*/
// TODO: May be rename. e.g. intersects for something else.
open fun first(set: AbstractSet): Boolean = true
/**
* This method is used for replacement backreferenced sets.
*
* @return null if current node need not to be replaced,
* [JointSet] which is replacement of current node otherwise.
*/
open fun processBackRefReplacement(): JointSet? {
return null
}
/**
* This method performs the second pass without checking if it's already performed or not.
*/
open fun processSecondPassInternal(): AbstractSet {
if (!next.secondPassVisited) {
this.next = next.processSecondPass()
}
return processBackRefReplacement() ?: this
}
/**
* This method is used for traversing nodes after the first stage of compilation.
*/
open fun processSecondPass(): AbstractSet {
secondPassVisited = true
return processSecondPassInternal()
}
}
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* This class represent atomic group (?>X), once X matches, this match become unchangeable till the end of the match.
*
* @author Nikolay A. Kuznetsov
*/
open internal class AtomicJointSet(children: List<AbstractSet>, fSet: FSet) : NonCapturingJointSet(children, fSet) {
/** Returns startIndex+shift, the next position to match */
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val start = matchResult.getConsumed(groupIndex)
matchResult.setConsumed(groupIndex, startIndex)
children.forEach {
val shift = it.matches(startIndex, testString, matchResult)
if (shift >= 0) {
// AtomicFset always returns true, but saves the index to run this next.match() from;
// TODO: Try to get rid of this explicit cast to AtomicFSet
return next.matches((fSet as AtomicFSet).index, testString, matchResult) // TODO: We use next here.
}
}
matchResult.setConsumed(groupIndex, start)
return -1
}
override val name: String
get() = "AtomicJointSet"
// TODO: looks like we can replace it with just next property.
override var next: AbstractSet = dummyNext
}
@@ -0,0 +1,95 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Case Insensitive back reference node;
*
* @author Nikolay A. Kuznetsov
*/
// TODO: rename the properties
open internal class BackReferenceSet(val referencedGroup: Int, val consCounter: Int, val ignoreCase: Boolean = false)
: SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val groupValue = getReferencedGroupValue(matchResult)
if (groupValue == null || startIndex + groupValue.length > testString.length) {
return -1
}
if (testString.startsWith(groupValue, startIndex, ignoreCase)) {
matchResult.setConsumed(consCounter, groupValue.length)
return next.matches(startIndex + groupValue.length, testString, matchResult)
}
return -1
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val groupValue = getReferencedGroupValue(matchResult)
if (groupValue == null || startIndex + groupValue.length > testString.length) {
return -1
}
var index = startIndex
while (index <= testString.length) {
index = testString.indexOf(groupValue, index, ignoreCase)
if (index < 0) {
return -1
}
if (index < testString.length
&& next.matches(index + groupValue.length, testString, matchResult) >=0) {
return index
}
index++
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val groupValue = getReferencedGroupValue(matchResult)
if (groupValue == null || leftLimit + groupValue.length > rightLimit) {
return -1
}
var index = rightLimit
while (index >= leftLimit) {
index = testString.lastIndexOf(groupValue, index, ignoreCase)
if (index < 0) {
return -1
}
if (index >= 0 && next.matches(index + groupValue.length, testString, matchResult) >= 0) {
return index
}
index--
}
return -1
}
protected fun getReferencedGroupValue(matchResult: MatchResultImpl) = matchResult.group(referencedGroup)
override val name: String
get() = "back reference: $referencedGroup"
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
val result = matchResult.getConsumed(consCounter) != 0
matchResult.setConsumed(consCounter, -1)
return result
}
}
@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents node accepting single character.
*
* @author Nikolay A. Kuznetsov
*/
// TODO: Process next better - it is non-null in almost all cases.
open internal class CharSet(char: Char, val ignoreCase: Boolean = false) : LeafSet() {
// We use only low case characters when working in case insensitive mode.
val char: Char = if (ignoreCase) char.toLowerCase() else char
// Overrides =======================================================================================================
override fun accepts(startIndex: Int, testString: CharSequence): Int {
if (ignoreCase) {
return if (this.char == testString[startIndex].toLowerCase()) 1 else -1
} else {
return if (this.char == testString[startIndex]) 1 else -1
}
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
while (index < testString.length) {
index = testString.indexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
if (next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index++
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = rightLimit
while (index >= leftLimit) {
index = testString.lastIndexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
if (next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index--
}
return -1
}
override val name: String
get()= char.toString()
override fun first(set: AbstractSet): Boolean {
if (ignoreCase) {
return super.first(set) // TODO: Add correct checks here.
}
return when (set) {
is CharSet -> set.char == char
is RangeSet -> set.accepts(0, char.toString()) > 0
is SupplementaryCharSet -> false
is SupplementaryRangeSet -> set.contains(char)
else -> true
}
}
}
@@ -0,0 +1,119 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/*
*
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
*
* COPYRIGHT AND PERMISSION NOTICE
*
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
* hereby granted, free of charge, to any person obtaining a copy of the
* Unicode data files and any associated documentation (the "Data Files")
* or Unicode software and any associated documentation (the "Software")
* to deal in the Data Files or Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute,
* and/or sell copies of the Data Files or Software, and to permit persons
* to whom the Data Files or Software are furnished to do so, provided that
* (a) the above copyright notice(s) and this permission notice appear with
* all copies of the Data Files or Software, (b) both the above copyright
* notice(s) and this permission notice appear in associated documentation,
* and (c) there is clear notice in each modified Data File or in the Software
* as well as in the documentation associated with the Data File(s) or Software
* that the data or software has been modified.
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
*
* Except as contained in this notice, the name of a copyright holder shall
* not be used in advertising or otherwise to promote the sale, use or other
* dealings in these Data Files or Software without prior written
* authorization of the copyright holder.
*
* 2. Additional terms from the Database:
*
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
*
* Disclaimer
*
* The Unicode Character Database is provided as is by Unicode, Inc.
* No claims are made as to fitness for any particular purpose. No warranties
* of any kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been purchased
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
* will be exchange of defective media within 90 days of receipt. This disclaimer
* is applicable for all other data files accompanying the Unicode Character Database,
* some of which have been compiled by the Unicode Consortium, and some of which
* have been supplied by other sources.
*
* Limitations on Rights to Redistribute This Data
*
* Recipient is granted the right to make copies in any form for internal
* distribution and to freely use the information supplied in the creation of
* products supporting the UnicodeTM Standard. The files in
* the Unicode Character Database can be redistributed to third parties or other
* organizations (whether for profit or not) as long as this notice and the disclaimer
* notice are retained. Information can be extracted from these files and used
* in documentation or programs, as long as there is an accompanying notice
* indicating the source.
*/
package kotlin.text
/**
* This class is used to split the range that contains surrogate characters into two ranges:
* the first consisting of these surrogate characters and the second consisting of all others characters
* from the parent range. This class represents the parent range split in such a manner.
*/
// TODO: May be we can merge sets with surrogates and without them.
// TODO: Use more specific constructor params (e.g. RangeSet)
internal class CompositeRangeSet(/* range without surrogates */ val withoutSurrogates: AbstractSet,
/* range containing surrogates only */ val surrogates: AbstractSet) : SimpleSet() {
override var next: AbstractSet = dummyNext
get() = field
set(next) {
field = next
surrogates.next = next
withoutSurrogates.next = next
}
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var result = withoutSurrogates.matches(startIndex, testString, matchResult)
if (result < 0) {
result = surrogates.matches(startIndex, testString, matchResult)
}
return result
}
override val name: String
get() = "CompositeRangeSet: " + " <nonsurrogate> " + withoutSurrogates + " <surrogate> " + surrogates
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
override fun first(set: AbstractSet): Boolean {
return true
}
}
@@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/** Represents canonical decomposition of Unicode character. Is used when CANON_EQ flag of Pattern class is specified. */
// TODO: Refactor it.
open internal class DecomposedCharSet(
/** Decomposition of the Unicode codepoint */
private val decomposedChar: IntArray,
/** Length of useful part of decomposedChar decomposedCharLength <= decomposedChar.length */
private val decomposedCharLength: Int) : SimpleSet() {
/** Contains information about number of chars that were read for a codepoint last time */
private var readCharsForCodePoint = 1
/** UTF-16 encoding of decomposedChar */
private val decomposedCharUTF16: String by lazy {
val strBuff = StringBuilder()
for (i in 0..decomposedCharLength - 1) {
strBuff.append(Char.toChars(decomposedChar[i]))
}
return@lazy strBuff.toString()
}
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var strIndex = startIndex
val rightBound = testString.length
if (strIndex >= rightBound) {
return -1
}
// We read testString and decompose it gradually to compare with this decomposedChar at position strIndex
var curChar = codePointAt(strIndex, testString, rightBound)
strIndex += readCharsForCodePoint
var decomposedCurrentCodePoint: IntArray? = Lexer.getDecomposition(curChar)
var readCodePoints = 0
var i = 0
// All decompositions have length that is less or equal Lexer.MAX_DECOMPOSITION_LENGTH
var decomposedCodePoint: IntArray
if (decomposedCurrentCodePoint == null) {
decomposedCodePoint = IntArray(Lexer.MAX_DECOMPOSITION_LENGTH)
decomposedCodePoint[readCodePoints++] = curChar
} else {
i = decomposedCurrentCodePoint.size
decomposedCodePoint = decomposedCurrentCodePoint.copyOf(Lexer.MAX_DECOMPOSITION_LENGTH)
readCodePoints += i
}
if (strIndex < rightBound) {
curChar = codePointAt(strIndex, testString, rightBound)
// Read testString until we met a decomposed char boundary and decompose obtained portion of testString.
while (readCodePoints < Lexer.MAX_DECOMPOSITION_LENGTH && !Lexer.isDecomposedCharBoundary(curChar)) {
if (!Lexer.hasDecompositionNonNullCanClass(curChar)) {
decomposedCodePoint[readCodePoints++] = curChar
} else {
/*
* A few codepoints have decompositions and non null
* canonical classes, we have to take them into
* consideration, but general rule is:
* if canonical class != 0 then no decomposition
*/
decomposedCurrentCodePoint = Lexer.getDecomposition(curChar)
/*
* Length of such decomposition is 1 or 2. See UnicodeData file
* http://www.unicode.org/Public/4.0-Update/UnicodeData-4.0.0.txt
*/
// hasDecompositionNonNullCanClass(curChar) == true, so decomposedCurrentCodePoint != null.
if (decomposedCurrentCodePoint!!.size == 2) {
decomposedCodePoint[readCodePoints++] = decomposedCurrentCodePoint[0]
decomposedCodePoint[readCodePoints++] = decomposedCurrentCodePoint[1]
} else {
decomposedCodePoint[readCodePoints++] = decomposedCurrentCodePoint[0]
}
}
strIndex += readCharsForCodePoint
if (strIndex < rightBound) {
curChar = codePointAt(strIndex, testString, rightBound)
} else {
break
}
}
}
// Some optimization since length of decomposed char is <= 3 usually
when (readCodePoints) {
0, 1, 2 -> {}
3 -> {
var i1 = Lexer.getCanonicalClass(decomposedCodePoint[1])
val i2 = Lexer.getCanonicalClass(decomposedCodePoint[2])
if (i2 != 0 && i1 > i2) {
i1 = decomposedCodePoint[1]
decomposedCodePoint[1] = decomposedCodePoint[2]
decomposedCodePoint[2] = i1
}
}
else -> decomposedCodePoint = Lexer.getCanonicalOrder(decomposedCodePoint, readCodePoints)
}
// Compare decomposedChar with decomposed char that was just read from testString
if (readCodePoints != decomposedCharLength) {
return -1
}
if ((0 until readCodePoints).firstOrNull { decomposedCodePoint[i] != decomposedChar[i] } != null) {
return -1
}
return next.matches(strIndex, testString, matchResult)
}
override val name: String
get() = "decomposed char: $decomposedChar"
/** Reads Unicode codepoint from [testString] starting from [strIndex] until [rightBound]. */
fun codePointAt(strIndex: Int, testString: CharSequence, rightBound: Int): Int {
var index = strIndex
// We store information about number of codepoints we read at variable readCharsForCodePoint.
val curChar: Int
readCharsForCodePoint = 1
if (index < rightBound - 1) {
val high = testString[index++]
val low = testString[index]
if (Char.isSurrogatePair(high, low)) {
curChar = Char.toCodePoint(high, low)
readCharsForCodePoint = 2
} else {
curChar = high.toInt()
}
} else {
curChar = testString[index].toInt()
}
return curChar
}
override fun first(set: AbstractSet): Boolean {
return if (set is DecomposedCharSet)
set.decomposedChar.contentEquals(decomposedChar)
else
true
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
}
@@ -0,0 +1,99 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Special node for ".*" construction.
* The main idea here is to find line terminator and try to find the rest of the construction from this point.
* @author Nikolay A. Kuznetsov
*/
// TODO: Add optimized implementation for '.+' case
internal class DotQuantifierSet(
innerSet: AbstractSet,
next: AbstractSet,
type: Int,
val lineTerminator: AbstractLineTerminator,
val matchLineTerminator: Boolean = false
) : QuantifierSet(innerSet, next, type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
val startSearch = if (matchLineTerminator) rightBound else testString.findLineTerminator(startIndex, rightBound)
if (startSearch <= startIndex) {
return if (type.toChar() == '+') {
-1
} else {
next.matches(startIndex, testString, matchResult)
}
}
val result = next.findBack(startIndex, startSearch, testString, matchResult)
if (type.toChar() == '+' && result == startIndex) {
return -1
}
return result
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
if (matchLineTerminator) {
val foundIndex = next.findBack(startIndex, rightBound, testString, matchResult)
if (foundIndex >= 0 && !(type.toChar() == '+' && foundIndex == startIndex)) {
return startIndex
} else {
return -1
}
} else {
// 1. find first occurrence of the searched pattern.
var nextFound = next.find(startIndex, testString, matchResult)
if (nextFound < 0) {
return -1
}
// 2. Check if we have other occurrences till the end of line (because .* is greedy and we need the last one).
val nextFoundLast = next.findBack(nextFound,
testString.findLineTerminator(nextFound, rightBound),
testString, matchResult)
nextFound = maxOf(nextFound, nextFoundLast)
// 3. Find the left boundary of this search.
val leftBound = findBackLineTerminator(startIndex, nextFound, testString)
if (type.toChar() == '+' && leftBound + 1 == nextFound) {
return -1
}
return leftBound + 1
}
}
/**
* Find the first line terminator between [from] (inclusive) and [to] (exclusive) indices.
* Returns [to] if no terminator found.
*/
private fun CharSequence.findLineTerminator(from: Int, to: Int): Int =
(from until to).firstOrNull { lineTerminator.isLineTerminator(this[it]) } ?: to
/**
* Find the first line terminator between [from] (inclusive) and [to] (exclusive) indices.
* Returns [from - 1] if no terminator found.
*/
private fun findBackLineTerminator(from: Int, to: Int, testString: CharSequence): Int =
(from until to).lastOrNull { lineTerminator.isLineTerminator(testString[it]) } ?: from - 1
override val name: String
get() = ".*"
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Node accepting any character except line terminators.
*
* @author Nikolay A. Kuznetsov
*/
// TODO: Looks like we can extend AbstractSet instead of JointSet here.
internal class DotSet(val lt: AbstractLineTerminator, val matchLineTerminator: Boolean)
: SimpleSet(AbstractSet.TYPE_DOTSET) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
if (startIndex >= rightBound) {
return -1
}
val high = testString[startIndex]
if (high.isHighSurrogate() && startIndex + 2 <= rightBound) {
val low = testString[startIndex + 1]
if (Char.isSurrogatePair(high, low)) {
if (!matchLineTerminator && lt.isLineTerminator(Char.toCodePoint(high, low))) {
return -1
} else {
return next.matches(startIndex + 2, testString, matchResult)
}
}
}
if (!matchLineTerminator && lt.isLineTerminator(high)) {
return -1
} else {
return next.matches(startIndex + 1, testString, matchResult)
}
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
override val name: String
get() = "."
}
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents end of input '\z', i.e. matches only character after the last one;
*
* @author Nikolay A. Kuznetsov
*/
internal class EOISet : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (startIndex < testString.length) {
return -1
}
return next.matches(startIndex, testString, matchResult)
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
override val name: String
get() = "EOI"
}
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents a node for a '$' sign.
* Note: In Kotlin we use only the "anchoring bounds" mode when "$" matches the end of a match region.
* See: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#useAnchoringBounds-boolean-
*
* @author Nikolay A. Kuznetsov
*/
// TODO: What is consCounter?
internal class EOLSet(val consCounter: Int, val lt: AbstractLineTerminator, val multiline: Boolean = false) : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
val remainingChars = rightBound - startIndex
when {
startIndex >= rightBound ||
remainingChars == 1 && lt.isLineTerminator(testString[startIndex]) ||
remainingChars == 2 && lt.isLineTerminatorPair(testString[startIndex], testString[startIndex+1]) ||
multiline && lt.isLineTerminator(testString[startIndex]) -> {
matchResult.setConsumed(consCounter, 0)
return next.matches(startIndex, testString, matchResult)
}
}
return -1
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
val result = matchResult.getConsumed(consCounter) != 0
matchResult.setConsumed(consCounter, -1)
return result
}
override val name: String
get()= "<EOL>"
}
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Valid constant zero character match.
*
* @author Nikolay A. Kuznetsov
*/
internal class EmptySet(override var next: AbstractSet) : LeafSet() {
override val charCount = 0
override fun accepts(startIndex: Int, testString: CharSequence): Int = 0
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
for (index in startIndex..testString.length) {
if (index < testString.length) {
if (testString[index].isLowSurrogate() &&
index > 0 && testString[index - 1].isHighSurrogate()) {
continue
}
}
if (next.matches(index, testString, matchResult) >= 0) {
return index
}
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
for (index in rightLimit downTo leftLimit) {
if (index < testString.length) {
if (testString[index].isLowSurrogate() &&
index > 0 && testString[index - 1].isHighSurrogate()) {
continue
}
}
if (next.matches(index, testString, matchResult) >= 0) {
return index
}
}
return -1
}
override val name: String
get()= "<Empty set>"
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return false
}
}
@@ -0,0 +1,164 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* The node which marks end of the particular group.
* @author Nikolay A. Kuznetsov
*/
// TODO: Rename?
open internal class FSet(val groupIndex: Int) : SimpleSet() {
var isBackReferenced = false
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val oldEnd = matchResult.getEnd(groupIndex)
matchResult.setEnd(groupIndex, startIndex)
val shift = next.matches(startIndex, testString, matchResult)
if (shift < 0) {
matchResult.setEnd(groupIndex, oldEnd)
}
return shift
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
override val name: String
get() = "fSet"
override fun processSecondPass(): FSet {
val result = super.processSecondPass()
assert(result == this)
return this
}
/**
* Marks the end of the particular group and not take into account possible
* kickbacks (required for atomic groups, for instance)
*/
internal class PossessiveFSet : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
return startIndex
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return false
}
override val name: String
get() = "possessiveFSet"
}
companion object {
var possessiveFSet = PossessiveFSet()
}
}
/**
* Special construction which marks end of pattern.
*
* @author Nikolay A. Kuznetsov
*/
internal class FinalSet : FSet(0) {
override fun matches(startIndex: Int, testString: CharSequence,
matchResult: MatchResultImpl): Int {
if (matchResult.mode == Regex.Mode.FIND || startIndex == testString.length) {
matchResult.setEnd(0, startIndex)
return startIndex
}
return -1
}
override val name: String
get() = "FinalSet"
}
/**
* Non-capturing group closing node.
*
* @author Nikolay A. Kuznetsov
*/
internal class NonCapFSet(groupIndex: Int) : FSet(groupIndex) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
matchResult.setConsumed(groupIndex, startIndex - matchResult.getConsumed(groupIndex)) // TODO: Don't understand it.
return next.matches(startIndex, testString, matchResult)
}
override val name: String
get() = "NonCapFSet"
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return false
}
}
/**
* LookAhead FSet, always returns true
* @author Nikolay A. Kuznetsov
*/
internal class AheadFSet : FSet(-1) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
return startIndex
}
override val name: String
get() = "AheadFSet"
}
/**
* FSet for lookbehind constructs. Checks if string index saved by corresponding
* jointSet in "consumers" equals to current index and return current string
* index, return -1 otherwise.
* @author Nikolay A. Kuznetsov
*/
internal class BehindFSet(groupIndex: Int) : FSet(groupIndex) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = matchResult.getConsumed(groupIndex)
return if (rightBound == startIndex) startIndex else -1
}
override val name: String
get() = "BehindFSet"
}
/**
* Represents an end of an atomic group.
*
* @author Nikolay A. Kuznetsov
*/
internal class AtomicFSet(groupIndex: Int) : FSet(groupIndex) {
var index: Int = 0
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
matchResult.setConsumed(groupIndex, startIndex - matchResult.getConsumed(groupIndex))
index = startIndex
return startIndex
}
override val name: String
get() = "AtomicFSet"
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return false
}
}
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Default quantifier over groups, in fact this type of quantifier is
* generally used for constructions we cant identify number of characters they
* consume.
* @author Nikolay A. Kuznetsov
*/
open internal class GroupQuantifierSet(
val quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int,
val groupQuantifierIndex: Int // It's used to remember a number of the innerSet occurrences during the recursive search.
) : QuantifierSet(innerSet, next, type) {
val max: Int get() = quantifier.max
val min: Int get() = quantifier.min
// We call innerSet.matches here, if it succeeds it call next.matches where next is this QuantifierSet.
// So we have a recursive searching procedure.
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (!innerSet.hasConsumed(matchResult)) {
return next.matches(startIndex, testString, matchResult)
}
// TODO: We can store an optimized functions for specific cases (like *) in a separate callable reference and
// TODO: call it instead of checks during matching.
// Fast case: '*' or {0, } - no need to count occurrences.
if (min == 0 && max == Quantifier.INF) {
val nextIndex = innerSet.matches(startIndex, testString, matchResult)
return if (nextIndex < 0) {
next.matches(startIndex, testString, matchResult)
} else {
nextIndex
}
}
val enterCount = matchResult.enterCounters[groupQuantifierIndex]
// can't go inner set;
if (max != Quantifier.INF && enterCount >= max) {
return next.matches(startIndex, testString, matchResult)
}
// go inner set;
matchResult.enterCounters[groupQuantifierIndex]++
val nextIndex = innerSet.matches(startIndex, testString, matchResult)
if (nextIndex < 0) {
matchResult.enterCounters[groupQuantifierIndex]--
if (enterCount >= min) {
return next.matches(startIndex, testString, matchResult)
} else {
matchResult.enterCounters[groupQuantifierIndex] = 0
return -1
}
} else {
matchResult.enterCounters[groupQuantifierIndex] = 0
return nextIndex
}
}
override val name: String
get() = quantifier.toString()
}
@@ -0,0 +1,173 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents canonical decomposition of Hangul syllable. Is used when
* CANON_EQ flag of Pattern class is specified.
*/
// TODO: Refactor it.
internal class HangulDecomposedCharSet(
/**
* Decomposed Hangul syllable.
*/
private val decomposedChar: CharArray,
/**
* Length of useful part of decomposedChar
* decomposedCharLength <= decomposedChar.length
*/
private val decomposedCharLength: Int) : SimpleSet() {
/**
* String representing syllable
*/
private val decomposedCharUTF16: String by lazy {
fromCharArray(decomposedChar, 0, decomposedChar.size)
}
override val name: String
get() = "decomposed Hangul syllable: $decomposedCharUTF16"
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
/*
* All decompositions for Hangul syllables have length that
* is less or equal Lexer.MAX_DECOMPOSITION_LENGTH
*/
val rightBound = testString.length
var SyllIndex = 0
val decompSyllable = IntArray(Lexer
.MAX_HANGUL_DECOMPOSITION_LENGTH)
val decompCurSymb: IntArray?
var curSymb: Char
/*
* For details about Hangul composition and decomposition see
* http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf
* "3.12 Conjoining Jamo Behavior"
*/
var LIndex = -1
var VIndex = -1
var TIndex = -1
if (index >= rightBound) {
return -1
}
curSymb = testString[index++]
decompCurSymb = Lexer.getHangulDecomposition(curSymb.toInt())
if (decompCurSymb == null) {
/*
* We deal with ordinary letter or sequence of jamos
* at index at testString.
*/
decompSyllable[SyllIndex++] = curSymb.toInt()
LIndex = curSymb.toInt() - Lexer.LBase
if (LIndex < 0 || LIndex >= Lexer.LCount) {
/*
* Ordinary letter, that doesn't match this
*/
return -1
}
if (index < rightBound) {
curSymb = testString[index]
VIndex = curSymb.toInt() - Lexer.VBase
}
if (VIndex < 0 || VIndex >= Lexer.VCount) {
/*
* Single L jamo doesn't compose Hangul syllable,
* so doesn't match
*/
return -1
}
index++
decompSyllable[SyllIndex++] = curSymb.toInt()
if (index < rightBound) {
curSymb = testString[index]
TIndex = curSymb.toInt() - Lexer.TBase
}
if (TIndex < 0 || TIndex >= Lexer.TCount) {
/*
* We deal with LV syllable at testString, so
* compare it to this
*/
return if (decomposedCharLength == 2
&& decompSyllable[0] == decomposedChar[0].toInt()
&& decompSyllable[1] == decomposedChar[1].toInt())
next.matches(index, testString, matchResult)
else
-1
}
index++
decompSyllable[SyllIndex++] = curSymb.toInt()
/*
* We deal with LVT syllable at testString, so
* compare it to this
*/
return if (decomposedCharLength == 3
&& decompSyllable[0] == decomposedChar[0].toInt()
&& decompSyllable[1] == decomposedChar[1].toInt()
&& decompSyllable[2] == decomposedChar[2].toInt())
next.matches(index, testString, matchResult)
else
-1
} else {
/*
* We deal with Hangul syllable at index at testString.
* So we decomposed it to compare with this.
*/
var i = 0
if (decompCurSymb.size != decomposedCharLength) {
return -1
}
while (i < decomposedCharLength) {
if (decompCurSymb[i] != decomposedChar[i].toInt()) {
return -1
}
i++
}
return next.matches(index, testString, matchResult)
}
}
override fun first(set: AbstractSet): Boolean {
return if (set is HangulDecomposedCharSet)
set.decomposedCharUTF16 == decomposedCharUTF16
else
true
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return true
}
}
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents group, which is alternation of other subexpression.
* One should think about "group" in this model as JointSet opening group and corresponding FSet closing group.
*
* @author Nikolay A. Kuznetsov
*/
open internal class JointSet(children: List<AbstractSet>, fSet: FSet) : AbstractSet() {
protected var children: MutableList<AbstractSet> = mutableListOf<AbstractSet>().apply { addAll(children) }
var fSet: FSet = fSet
protected set
var groupIndex: Int = fSet.groupIndex
protected set
/**
* Returns startIndex+shift, the next position to match
*/
// TODO: Why don't we use a next here?
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (children.isEmpty()) {
return -1
}
val oldStart = matchResult.getStart(groupIndex)
matchResult.setStart(groupIndex, startIndex)
children.forEach {
val shift = it.matches(startIndex, testString, matchResult)
if (shift >= 0) {
return shift
}
}
matchResult.setStart(groupIndex, oldStart)
return -1
}
override var next: AbstractSet
get() = fSet.next
set(next) {
fSet.next = next
}
override val name: String
get() = "JointSet"
override fun first(set: AbstractSet): Boolean = children.any { it.first(set) }
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return !(matchResult.getEnd(groupIndex) >= 0 && matchResult.getStart(groupIndex) == matchResult.getEnd(groupIndex))
}
override fun processSecondPassInternal(): AbstractSet {
val fSet = this.fSet
if (!fSet.secondPassVisited) {
val newFSet = fSet.processSecondPass()
assert(newFSet == fSet)
}
children.replaceAll { child -> if (!child.secondPassVisited) child.processSecondPass() else child }
return super.processSecondPassInternal()
}
}
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 kotlin.RuntimeException
/**
* Generalized greedy quantifier node over the leaf nodes.
* - a{n,m};
* - a* == a{0, <inf>};
* - a? == a{0, 1};
* - a+ == a{1, <inf>};
*
* @author Nikolay A. Kuznetsov
*/
open internal class LeafQuantifierSet(var quantifier: Quantifier,
innerSet: LeafSet,
next: AbstractSet,
type: Int
) : QuantifierSet(innerSet, next, type) {
val leaf: LeafSet get() = super.innerSet as LeafSet
val min: Int get() = quantifier.min
val max: Int get() = quantifier.max
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
var occurrences = 0
// Process first <min> occurrences of the sequence being looked for.
while (occurrences < min) {
if (index + leaf.charCount > testString.length) {
return -1
}
val shift = leaf.accepts(index, testString)
if (shift < 1) {
return -1
}
index += shift
occurrences++
}
// Process occurrences between min and max.
while ((max < 0 || occurrences < max) && index + leaf.charCount <= testString.length) {
val shift = leaf.accepts(index, testString)
if (shift < 1) {
break
}
index += shift
occurrences++
}
// Roll back if the next node does't match the remaining string.
while (occurrences >= min) {
val shift = next.matches(index, testString, matchResult)
if (shift >= 0) {
return shift
}
index -= leaf.charCount
occurrences--
}
return -1
}
override val name: String
get() = quantifier.toString()
override var innerSet: AbstractSet
get() = super.innerSet
set(innerSet) {
if (innerSet !is LeafSet)
throw RuntimeException()
super.innerSet = innerSet
}
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Base class for nodes representing leaf tokens of the RE, those who consumes fixed number of characters.
*
* @author Nikolay A. Kuznetsov
*/
internal abstract class LeafSet : SimpleSet(AbstractSet.TYPE_LEAF) {
open val charCount = 1
/**
* Returns "shift", the number of accepted chars.
* Commonly internal function, but called by quantifiers.
*/
// TODO: Magic function. Use by another way?
abstract fun accepts(startIndex: Int, testString: CharSequence): Int
/**
* Checks if we can enter this state and pass the control to the next one.
* Return positive value if match succeeds, negative otherwise.
*/
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (startIndex + charCount > testString.length) {
return -1
}
val shift = accepts(startIndex, testString) // TODO: may be move the check above in accept function.
if (shift < 0) {
return -1
}
return next.matches(startIndex + shift, testString, matchResult)
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return true
}
}
@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Positive lookahead node.
*
* @author Nikolay A. Kuznetsov
*/
internal class PositiveLookAheadSet(children: List<AbstractSet>, fSet: FSet) : AtomicJointSet(children, fSet) {
/** Returns startIndex+shift, the next position to match */
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
children.forEach {
val shift = it.matches(startIndex, testString, matchResult)
if (shift >= 0) {
// PosLookaheadFset always returns true, position remains the same next.match() from;
return next.matches(startIndex, testString, matchResult)
}
}
return -1
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true // TODO: May be false?
override val name: String
get() = "PositiveLookaheadJointSet"
}
/**
* Negative look ahead node.
*
* @author Nikolay A. Kuznetsov
*/
internal class NegativeLookAheadSet(children: List<AbstractSet>, fSet: FSet) : AtomicJointSet(children, fSet) {
/** Returns startIndex+shift, the next position to match */
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
children.forEach {
if (it.matches(startIndex, testString, matchResult) >= 0) {
return -1
}
}
return next.matches(startIndex, testString, matchResult)
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
override val name: String
get() = "NegativeLookaheadJointSet"
}
@@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Positive lookbehind node.
*
* @author Nikolay A. Kuznetsov
*/
internal class PositiveLookBehindSet(children: List<AbstractSet>, fSet: FSet) : AtomicJointSet(children, fSet) {
/** Returns startIndex+shift, the next position to match */
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
matchResult.setConsumed(groupIndex, startIndex)
children.forEach {
if (it.findBack(0, startIndex, testString, matchResult) >= 0) {
matchResult.setConsumed(groupIndex, -1)
return next.matches(startIndex, testString, matchResult)
}
}
return -1
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true // TODO: False was here. It's true just for experiment.
override val name: String
get() = "PositiveBehindJointSet"
}
/**
* Negative look behind node.
*
* @author Nikolay A. Kuznetsov
*/
internal class NegativeLookBehindSet(children: List<AbstractSet>, fSet: FSet) : AtomicJointSet(children, fSet) {
/** Returns startIndex+shift, the next position to match */
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
matchResult.setConsumed(groupIndex, startIndex)
children.forEach {
val shift = it.findBack(0, startIndex, testString, matchResult)
if (shift >= 0) {
return -1
}
}
return next.matches(startIndex, testString, matchResult)
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
override val name: String
get() = "NegativeBehindJointSet"
}
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Node representing non-capturing group
* @author Nikolay A. Kuznetsov
*/
open internal class NonCapturingJointSet(children: List<AbstractSet>, fSet: FSet) : JointSet(children, fSet) {
/**
* Returns startIndex+shift, the next position to match
*/
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val start = matchResult.getConsumed(groupIndex)
matchResult.setConsumed(groupIndex, startIndex)
children.forEach {
val shift = it.matches(startIndex, testString, matchResult)
if (shift >= 0) {
return shift
}
}
matchResult.setConsumed(groupIndex, start)
return -1
}
override val name: String
get() = "NonCapturingJointSet"
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
return matchResult.getConsumed(groupIndex) != 0
}
}
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Possessive quantifier set over groups.
*
* @author Nikolay A. Kuznetsov
*/
internal class PossessiveGroupQuantifierSet(
quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int,
setCounter: Int
): GroupQuantifierSet(quantifier, innerSet, next, type, setCounter) {
init {
innerSet.next = FSet.possessiveFSet // TODO: Try to use such an approach for other sets.
}
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
var nextIndex: Int = innerSet.matches(index, testString, matchResult)
var occurrences = 0
while (nextIndex > index && (max == Quantifier.INF || occurrences < max)) {
occurrences++
index = nextIndex
nextIndex = innerSet.matches(index, testString, matchResult)
}
// TODO: Do we need this check?
if (/* nextIndex < 0 && */ occurrences < quantifier.min) {
return -1
} else {
return next.matches(index, testString, matchResult)
}
}
}
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Possessive quantifier node over a leaf node.
* - a{n,m}+;
* - a*+ == a{0, <inf>}+;
* - a?+ == a{0, 1}+;
* - a++ == a{1, <inf>}+;
*
* @author Nikolay A. Kuznetsov
*/
internal class PossessiveLeafQuantifierSet(
quant: Quantifier,
innerSet: LeafSet,
next: AbstractSet, type: Int
) : LeafQuantifierSet(quant, innerSet, next, type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
var occurrences = 0
// TODO: Create a separate functions for these loops?
while (occurrences < min) {
if (index + leaf.charCount > testString.length) {
return -1
}
val shift = leaf.accepts(index, testString)
if (shift < 1) {
return -1
}
index += shift
occurrences++
}
while ((max >= 0 || occurrences < max) && index + leaf.charCount <= testString.length) {
val shift = leaf.accepts(index, testString)
if (shift < 1) {
break
}
index += shift
occurrences++
}
return next.matches(index, testString, matchResult)
}
}
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Node representing previous match (\G).
*
* @author Nikolay A. Kuznetsov
*/
internal class PreviousMatchSet : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (startIndex == matchResult.previousMatch) {
return next.matches(startIndex, testString, matchResult)
}
return -1
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
override val name: String
get() = "PreviousMatchSet"
}
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Base class for quantifiers.
*
* @author Nikolay A. Kuznetsov
*/
// TODO: check of innerSet must be nullable or not
internal abstract class QuantifierSet(open var innerSet: AbstractSet, override var next: AbstractSet, type: Int)
: SimpleSet(type) {
// TODO: Looks like we need true by default here.
override fun first(set: AbstractSet): Boolean =
innerSet.first(set) || next.first(set)
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
override fun processSecondPassInternal(): AbstractSet {
val innerSet = this.innerSet
if (innerSet.secondPassVisited) {
this.innerSet = innerSet.processSecondPass()
}
return super.processSecondPassInternal()
}
}
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents node accepting single character from the given char class. (TODO: Not a surrogate character?)
*
* @author Nikolay A. Kuznetsov
*/
open internal class RangeSet(charClass: AbstractCharClass, val ignoreCase: Boolean = false) : LeafSet() {
val chars: AbstractCharClass = charClass.instance
override fun accepts(startIndex: Int, testString: CharSequence): Int {
if (ignoreCase) {
val char = testString[startIndex]
return if (chars.contains(char.toUpperCase()) || chars.contains(char.toLowerCase())) 1 else -1
} else {
return if (chars.contains(testString[startIndex])) 1 else -1
}
}
override val name: String
get() = "range:" + (if (chars.alt) "^ " else " ") + chars.toString()
override fun first(set: AbstractSet): Boolean {
return when (set) {
is CharSet -> AbstractCharClass.intersects(chars, set.char.toInt())
is RangeSet -> AbstractCharClass.intersects(chars, set.chars)
is SupplementaryCharSet -> false
is SupplementaryRangeSet -> AbstractCharClass.intersects(chars, set.chars)
else -> true
}
}
}
@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Reluctant version of the group quantifier set.
*
* @author Nikolay A. Kuznetsov
*/
internal class ReluctantGroupQuantifierSet(
quantifier: Quantifier,
innerSet: AbstractSet,
next: AbstractSet,
type: Int,
setCounter: Int
) : GroupQuantifierSet(quantifier, innerSet, next, type, setCounter) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (!innerSet.hasConsumed(matchResult)) {
return next.matches(startIndex, testString, matchResult)
}
// Fast case: '*' or {0, } - no need to count occurrences.
if (min == 0 && max == Quantifier.INF) {
val res = next.matches(startIndex, testString, matchResult)
return if (res < 0) {
innerSet.matches(startIndex, testString, matchResult)
} else {
res
}
}
val enterCounter = matchResult.enterCounters[groupQuantifierIndex]
// can't go inner set;
if (enterCounter >= max) {
matchResult.enterCounters[groupQuantifierIndex] = 0 // TODO: Do we need it?
return next.matches(startIndex, testString, matchResult)
}
var nextIndex: Int
if (enterCounter >= min) {
nextIndex = next.matches(startIndex, testString, matchResult)
if (nextIndex < 0) {
matchResult.enterCounters[groupQuantifierIndex]++
nextIndex = innerSet.matches(startIndex, testString, matchResult)
} else {
matchResult.enterCounters[groupQuantifierIndex] = 0
return nextIndex
}
} else {
matchResult.enterCounters[groupQuantifierIndex]++
nextIndex = innerSet.matches(startIndex, testString, matchResult)
}
return nextIndex
}
}
@@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Reluctant quantifier node over a leaf node.
* - a{n,m}?;
* - a*? == a{0, <inf>}?;
* - a?? == a{0, 1}?;
* - a+? == a{1, <inf>}?;
*
* @author Nikolay A. Kuznetsov
*/
internal class ReluctantLeafQuantifierSet(
quant: Quantifier,
innerSet: LeafSet,
next: AbstractSet,
type: Int
) : LeafQuantifierSet(quant, innerSet, next, type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
var occurrences = 0
while (occurrences < min) {
if (index + leaf.charCount > testString.length) {
return -1
}
val shift = leaf.accepts(index, testString)
if (shift < 1) {
return -1
}
index += shift
occurrences++
}
// TODO: May be refactor this loop.
do {
var shift = next.matches(index, testString, matchResult)
if (shift >= 0) {
return shift
}
if (index + leaf.charCount <= testString.length) {
shift = leaf.accepts(index, testString)
index += shift
occurrences++
}
} while (shift >= 1 && occurrences <= max)
return -1
}
}
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* A node representing a '^' sign.
* Note: In Kotlin we use only the "anchoring bounds" mode when "^" matches beginning of a match region.
* See: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#useAnchoringBounds-boolean-
*
* @author Nikolay A. Kuznetsov
*/
internal class SOLSet(val lt: AbstractLineTerminator, val multiline: Boolean = false) : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
if (!multiline) {
if (startIndex == 0) {
return next.matches(startIndex, testString, matchResult)
}
} else {
// TODO: In Kotlin JVM the empty string doesn't match to "^.$" pattern in multiline mode and matches in single-line mode.
// TODO: So do we need to implement this behaviour or we can match to the pattern in the both cases?
if (startIndex != testString.length
&& (startIndex == 0
|| lt.isAfterLineTerminator(testString[startIndex - 1], testString[startIndex]))) {
return next.matches(startIndex, testString, matchResult)
}
}
return -1
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
override val name: String
get() = "^"
}
@@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* This class represents nodes constructed with character sequences. For
* example, lets consider regular expression: ".*word.*". During regular
* expression compilation phase character sequence w-o-r-d, will be represented
* with single node for the entire word.
*
* @author Nikolay A. Kuznetsov
*/
open internal class SequenceSet(substring: CharSequence, val ignoreCase: Boolean = false) : LeafSet() {
/** Represents a character sequence used for matching/searching. */
protected val patternString: String = substring.toString()
override val name: String= "sequence: " + patternString
override val charCount = substring.length
// Overrides =======================================================================================================
/** Returns true if [index] points to a low surrogate following a high surrogate */
private fun isLowSurrogateOfSupplement(string: CharSequence, index: Int): Boolean =
index < string.length && string[index].isLowSurrogate() && index > 0 && string[index - 1].isHighSurrogate()
override fun accepts(startIndex: Int, testString: CharSequence): Int {
return if (testString.startsWith(patternString, startIndex, ignoreCase)
&& !isLowSurrogateOfSupplement(testString, startIndex)
&& !isLowSurrogateOfSupplement(testString, startIndex + patternString.length)) {
charCount
} else {
-1
}
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
while (index < testString.length) {
index = testString.indexOf(patternString, index, ignoreCase)
if (index < 0) {
return -1
}
// Check if we have a supplementary code point at the beginning or at the end of the string.
if (!isLowSurrogateOfSupplement(testString, index)
&& !isLowSurrogateOfSupplement(testString, index + patternString.length)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index++
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = rightLimit
while (index >= leftLimit) {
index = testString.lastIndexOf(patternString, index, ignoreCase)
if (index < 0) {
return -1
}
// Check if we have a supplementary code point at the beginning or at the end of the string.
if (!isLowSurrogateOfSupplement(testString, index)
&& !isLowSurrogateOfSupplement(testString, index + patternString.length)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index--
}
return -1
}
// TODO: reread/rewrite
override fun first(set: AbstractSet): Boolean {
if (ignoreCase) {
return super.first(set) // TODO: Add correct checks for this case.
}
return when (set) {
is CharSet -> set.char == patternString[0]
is RangeSet -> set.accepts(0, patternString.substring(0, 1)) > 0
// TODO: perfrom a char to codepoint converter.
is SupplementaryRangeSet -> set.contains(patternString[0]) || patternString.length > 1 && set.contains(Char.toCodePoint(patternString[0], patternString[1]))
is SupplementaryCharSet -> if (patternString.length > 1) set.codePoint == Char.toCodePoint(patternString[0], patternString[1])
else false
else -> true
}
}
}
@@ -0,0 +1,151 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Group node over subexpression without alternations.
* @author Nikolay A. Kuznetsov
*/
open internal class SingleSet(var kid: AbstractSet, fSet: FSet) : JointSet(listOf(), fSet) {
var backReferencedSet: BackReferencedSingleSet? = null
// Overrides (API) =================================================================================================
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val start = matchResult.getStart(groupIndex)
matchResult.setStart(groupIndex, startIndex)
val shift = kid.matches(startIndex, testString, matchResult)
if (shift >= 0) {
return shift
}
matchResult.setStart(groupIndex, start)
return -1
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val res = kid.find(startIndex, testString, matchResult)
if (res >= 0)
matchResult.setStart(groupIndex, res)
return res
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val res = kid.findBack(leftLimit, rightLimit, testString, matchResult)
if (res >= 0)
matchResult.setStart(groupIndex, res)
return res
}
override fun first(set: AbstractSet): Boolean = kid.first(set)
// Second pass processing ==========================================================================================
override fun processBackRefReplacement(): JointSet? {
/**
* We will store a reference to created BackReferencedSingleSet
* in [backReferencedSet] field. This is needed to process replacement
* of sets correctly since sometimes we cannot renew all references to
* detachable set in the current point of traverse. See
* QuantifierSet and AbstractSet processSecondPass() methods for
* more details.
*/
val result = BackReferencedSingleSet(this)
backReferencedSet = result
return result
}
override fun processSecondPassInternal(): AbstractSet {
val fSet = this.fSet
if (fSet != null) {
this.fSet = fSet.processSecondPass()
}
kid = kid.processSecondPass()
return processBackRefReplacement() ?: this
}
/**
* This method is used for traversing nodes after the first stage of compilation.
*/
override fun processSecondPass(): AbstractSet {
if (secondPassVisited) {
val fSet = this.fSet
if (fSet is FSet && fSet.isBackReferenced) {
assert(backReferencedSet != null) // secondPassVisited
return backReferencedSet!!
}
}
secondPassVisited = true
return processSecondPassInternal()
}
// Backreferenced version of the class =============================================================================
/**
* Group node over subexpression without alternations.
* This node is used if current group is referenced via a backreference.
*/
internal class BackReferencedSingleSet(node: SingleSet) : SingleSet(node.kid, node.fSet as FSet) {
/*
* This class is needed only for overwriting find() and findBack() methods of SingleSet class, which is being
* back referenced. The following example explains the need for such substitution:
*
* Let's consider the pattern ".*(.)\\1".
* Leading .* works as follows: finds line terminator and runs findBack from that point.
* `findBack` method in its turn (in contrast to matches) sets group boundaries on the back trace.
* Thus at the point we try to match back reference(\\1) groups are not yet set.
*
* To fix this problem we replace backreferenced groups with instances of this class,
* which will use matches instead of find; this will affect performance, but ensure correctness of the match.
*/
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
for (index in startIndex..testString.length) {
val oldStart = matchResult.getStart(groupIndex)
matchResult.setStart(groupIndex, index)
val res = kid.matches(index, testString, matchResult)
if (res >= 0) {
return index
} else {
matchResult.setStart(groupIndex, oldStart)
}
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int,
testString: CharSequence, matchResult: MatchResultImpl): Int {
for (index in rightLimit downTo leftLimit) {
val oldStart = matchResult.getStart(groupIndex)
matchResult.setStart(groupIndex, index)
val res = kid.matches(index, testString, matchResult)
if (res >= 0) {
return index
} else {
matchResult.setStart(groupIndex, oldStart)
}
}
return -1
}
override fun processBackRefReplacement(): JointSet? = null
}
}
@@ -0,0 +1,108 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/*
*
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
*
* COPYRIGHT AND PERMISSION NOTICE
*
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
* hereby granted, free of charge, to any person obtaining a copy of the
* Unicode data files and any associated documentation (the "Data Files")
* or Unicode software and any associated documentation (the "Software")
* to deal in the Data Files or Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute,
* and/or sell copies of the Data Files or Software, and to permit persons
* to whom the Data Files or Software are furnished to do so, provided that
* (a) the above copyright notice(s) and this permission notice appear with
* all copies of the Data Files or Software, (b) both the above copyright
* notice(s) and this permission notice appear in associated documentation,
* and (c) there is clear notice in each modified Data File or in the Software
* as well as in the documentation associated with the Data File(s) or Software
* that the data or software has been modified.
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
*
* Except as contained in this notice, the name of a copyright holder shall
* not be used in advertising or otherwise to promote the sale, use or other
* dealings in these Data Files or Software without prior written
* authorization of the copyright holder.
*
* 2. Additional terms from the Database:
*
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
*
* Disclaimer
*
* The Unicode Character Database is provided as is by Unicode, Inc.
* No claims are made as to fitness for any particular purpose. No warranties
* of any kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been purchased
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
* will be exchange of defective media within 90 days of receipt. This disclaimer
* is applicable for all other data files accompanying the Unicode Character Database,
* some of which have been compiled by the Unicode Consortium, and some of which
* have been supplied by other sources.
*
* Limitations on Rights to Redistribute This Data
*
* Recipient is granted the right to make copies in any form for internal
* distribution and to freely use the information supplied in the creation of
* products supporting the UnicodeTM Standard. The files in
* the Unicode Character Database can be redistributed to third parties or other
* organizations (whether for profit or not) as long as this notice and the disclaimer
* notice are retained. Information can be extracted from these files and used
* in documentation or programs, as long as there is an accompanying notice
* indicating the source.
*/
package kotlin.text
/**
* Represents node accepting single supplementary codepoint.
*
* @author Nikolay A. Kuznetsov
*/
//assumes int value of this supplementary codepoint as a parameter
// TODO: Tests crash here. Investigate why.
internal class SupplementaryCharSet(val codePoint: Int, ignoreCase: Boolean)
: SequenceSet(fromCharArray(Char.toChars(codePoint), 0, 2), ignoreCase) {
override val name: String
get() = patternString
override fun first(set: AbstractSet): Boolean {
// TODO: replace with when
return when (set) {
is SupplementaryCharSet -> set.codePoint == codePoint
is SupplementaryRangeSet -> set.contains(codePoint)
is CharSet,
is RangeSet -> false
else -> true
}
}
}
@@ -0,0 +1,163 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/*
*
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
*
* COPYRIGHT AND PERMISSION NOTICE
*
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
* hereby granted, free of charge, to any person obtaining a copy of the
* Unicode data files and any associated documentation (the "Data Files")
* or Unicode software and any associated documentation (the "Software")
* to deal in the Data Files or Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute,
* and/or sell copies of the Data Files or Software, and to permit persons
* to whom the Data Files or Software are furnished to do so, provided that
* (a) the above copyright notice(s) and this permission notice appear with
* all copies of the Data Files or Software, (b) both the above copyright
* notice(s) and this permission notice appear in associated documentation,
* and (c) there is clear notice in each modified Data File or in the Software
* as well as in the documentation associated with the Data File(s) or Software
* that the data or software has been modified.
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
*
* Except as contained in this notice, the name of a copyright holder shall
* not be used in advertising or otherwise to promote the sale, use or other
* dealings in these Data Files or Software without prior written
* authorization of the copyright holder.
*
* 2. Additional terms from the Database:
*
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
*
* Disclaimer
*
* The Unicode Character Database is provided as is by Unicode, Inc.
* No claims are made as to fitness for any particular purpose. No warranties
* of any kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been purchased
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
* will be exchange of defective media within 90 days of receipt. This disclaimer
* is applicable for all other data files accompanying the Unicode Character Database,
* some of which have been compiled by the Unicode Consortium, and some of which
* have been supplied by other sources.
*
* Limitations on Rights to Redistribute This Data
*
* Recipient is granted the right to make copies in any form for internal
* distribution and to freely use the information supplied in the creation of
* products supporting the UnicodeTM Standard. The files in
* the Unicode Character Database can be redistributed to third parties or other
* organizations (whether for profit or not) as long as this notice and the disclaimer
* notice are retained. Information can be extracted from these files and used
* in documentation or programs, as long as there is an accompanying notice
* indicating the source.
*/
package kotlin.text
/**
* Represents node accepting single character from the given char class.
* This character can be supplementary (2 chars needed to represent) or from
* basic multilingual pane (1 needed char to represent it).
*
* @author Nikolay A. Kuznetsov
*/
open internal class SupplementaryRangeSet(charClass: AbstractCharClass, val ignoreCase: Boolean = false): SimpleSet() {
// TODO: may be add ignoreCase flag to CharClass
val chars = charClass.instance
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val rightBound = testString.length
if (startIndex >= rightBound) {
return -1
}
var index = startIndex
val high = testString[index++]
val result = next.matches(index, testString, matchResult)
if (contains(high) && result >= 0) {
return result
}
if (index < rightBound) {
val low = testString[index++]
if (Char.isSurrogatePair(high, low) && contains(Char.toCodePoint(high, low))) {
return next.matches(index, testString, matchResult)
}
}
return -1
}
fun contains(char: Char): Boolean {
if (ignoreCase) {
return chars.contains(char.toUpperCase()) || chars.contains(char.toLowerCase())
} else {
return chars.contains(char)
}
}
fun contains(char: Int): Boolean {
return chars.contains(char)
}
override val name: String
get() = "range:" + (if (chars.alt) "^ " else " ") + chars.toString()
override fun first(set: AbstractSet): Boolean {
return when(set) {
is SupplementaryCharSet -> AbstractCharClass.intersects(chars, set.codePoint)
is CharSet -> AbstractCharClass.intersects(chars, set.char.toInt())
is SupplementaryRangeSet -> AbstractCharClass.intersects(chars, set.chars)
is RangeSet -> AbstractCharClass.intersects(chars, set.chars)
else -> true
}
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
}
@@ -0,0 +1,218 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/*
*
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
*
* COPYRIGHT AND PERMISSION NOTICE
*
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
* hereby granted, free of charge, to any person obtaining a copy of the
* Unicode data files and any associated documentation (the "Data Files")
* or Unicode software and any associated documentation (the "Software")
* to deal in the Data Files or Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute,
* and/or sell copies of the Data Files or Software, and to permit persons
* to whom the Data Files or Software are furnished to do so, provided that
* (a) the above copyright notice(s) and this permission notice appear with
* all copies of the Data Files or Software, (b) both the above copyright
* notice(s) and this permission notice appear in associated documentation,
* and (c) there is clear notice in each modified Data File or in the Software
* as well as in the documentation associated with the Data File(s) or Software
* that the data or software has been modified.
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
*
* Except as contained in this notice, the name of a copyright holder shall
* not be used in advertising or otherwise to promote the sale, use or other
* dealings in these Data Files or Software without prior written
* authorization of the copyright holder.
*
* 2. Additional terms from the Database:
*
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
*
* Disclaimer
*
* The Unicode Character Database is provided as is by Unicode, Inc.
* No claims are made as to fitness for any particular purpose. No warranties
* of any kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been purchased
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
* will be exchange of defective media within 90 days of receipt. This disclaimer
* is applicable for all other data files accompanying the Unicode Character Database,
* some of which have been compiled by the Unicode Consortium, and some of which
* have been supplied by other sources.
*
* Limitations on Rights to Redistribute This Data
*
* Recipient is granted the right to make copies in any form for internal
* distribution and to freely use the information supplied in the creation of
* products supporting the UnicodeTM Standard. The files in
* the Unicode Character Database can be redistributed to third parties or other
* organizations (whether for profit or not) as long as this notice and the disclaimer
* notice are retained. Information can be extracted from these files and used
* in documentation or programs, as long as there is an accompanying notice
* indicating the source.
*/
package kotlin.text
/**
* This class represents low surrogate character.
*
* Note that we can use high and low surrogate characters
* that don't combine into supplementary code point.
* See http://www.unicode.org/reports/tr18/#Supplementary_Characters
*
* @author Nikolay A. Kuznetsov
*/
// TODO: Could we use just CharSet instead of this one?
internal class LowSurrogateCharSet(low: Char) : CharSet(low) {
// TODO: Remove bounds from MatchResultImpl.
override fun accepts(startIndex: Int, testString: CharSequence): Int {
val result = super.accepts(startIndex, testString)
if (result < 0 || testString.isHighSurrogate(startIndex - 1)) {
return -1
}
return result
}
private fun CharSequence.isHighSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
= (index in leftBound until rightBound && this[index].isHighSurrogate())
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
while (index < testString.length) {
index = testString.indexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
if (!testString.isHighSurrogate(index - 1)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index++
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = rightLimit
while (index >= leftLimit) {
index = testString.lastIndexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
if (!testString.isHighSurrogate(index - 1, leftLimit, rightLimit)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index--
}
return -1
}
override fun first(set: AbstractSet): Boolean {
return when(set) {
is LowSurrogateCharSet -> set.char == this.char
is CharSet,
is RangeSet,
is SupplementaryCharSet,
is SupplementaryRangeSet -> false
else -> true
}
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
}
/**
* This class represents high surrogate character.
*/
internal class HighSurrogateCharSet(high: Char) : CharSet(high) {
override fun accepts(startIndex: Int, testString: CharSequence): Int {
val result = super.accepts(startIndex, testString)
if (result < 0 || testString.isLowSurrogate(startIndex + 1)) {
return -1
}
return result
}
private fun CharSequence.isLowSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
= (index in leftBound until rightBound && this[index].isLowSurrogate())
// TODO: We have a similar code here, in LowSurrogateCharSet and in CharSet. Reuse it somehow.
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
while (index < testString.length) {
index = testString.indexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
// Remove params.
if (!testString.isLowSurrogate(index + 1)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index++
}
return -1
}
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = rightLimit
while (index >= leftLimit) {
index = testString.lastIndexOf(char, index, ignoreCase)
if (index < 0) {
return -1
}
if (!testString.isLowSurrogate(index + 1, leftLimit, rightLimit)
&& next.matches(index + charCount, testString, matchResult) >= 0) {
return index
}
index--
}
return -1
}
override fun first(set: AbstractSet): Boolean {
return when (set) {
is HighSurrogateCharSet -> set.char == this.char
is CharSet,
is RangeSet,
is SupplementaryCharSet,
is SupplementaryRangeSet -> false
else -> true
}
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
}
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/*
*
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
*
* COPYRIGHT AND PERMISSION NOTICE
*
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
* hereby granted, free of charge, to any person obtaining a copy of the
* Unicode data files and any associated documentation (the "Data Files")
* or Unicode software and any associated documentation (the "Software")
* to deal in the Data Files or Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute,
* and/or sell copies of the Data Files or Software, and to permit persons
* to whom the Data Files or Software are furnished to do so, provided that
* (a) the above copyright notice(s) and this permission notice appear with
* all copies of the Data Files or Software, (b) both the above copyright
* notice(s) and this permission notice appear in associated documentation,
* and (c) there is clear notice in each modified Data File or in the Software
* as well as in the documentation associated with the Data File(s) or Software
* that the data or software has been modified.
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
*
* Except as contained in this notice, the name of a copyright holder shall
* not be used in advertising or otherwise to promote the sale, use or other
* dealings in these Data Files or Software without prior written
* authorization of the copyright holder.
*
* 2. Additional terms from the Database:
*
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
*
* Disclaimer
*
* The Unicode Character Database is provided as is by Unicode, Inc.
* No claims are made as to fitness for any particular purpose. No warranties
* of any kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been purchased
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
* will be exchange of defective media within 90 days of receipt. This disclaimer
* is applicable for all other data files accompanying the Unicode Character Database,
* some of which have been compiled by the Unicode Consortium, and some of which
* have been supplied by other sources.
*
* Limitations on Rights to Redistribute This Data
*
* Recipient is granted the right to make copies in any form for internal
* distribution and to freely use the information supplied in the creation of
* products supporting the UnicodeTM Standard. The files in
* the Unicode Character Database can be redistributed to third parties or other
* organizations (whether for profit or not) as long as this notice and the disclaimer
* notice are retained. Information can be extracted from these files and used
* in documentation or programs, as long as there is an accompanying notice
* indicating the source.
*/
package kotlin.text
/*
* This class is a range that contains only surrogate characters.
*
* @author Nikolay A. Kuznetsov
*/
internal class SurrogateRangeSet(surrChars: AbstractCharClass) : RangeSet(surrChars) {
override fun accepts(startIndex: Int, testString: CharSequence): Int {
val result = super.accepts(startIndex, testString)
when {
result < 0 ||
testString.isHighSurrogate(startIndex - 1) && testString.isLowSurrogate(startIndex) ||
testString.isHighSurrogate(startIndex) && testString.isLowSurrogate(startIndex + 1) ->
return -1
}
return result
}
private fun CharSequence.isHighSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
= (index in leftBound until rightBound && this[index].isHighSurrogate())
private fun CharSequence.isLowSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
= (index in leftBound until rightBound && this[index].isLowSurrogate())
override fun first(set: AbstractSet): Boolean {
return when (set) {
is SurrogateRangeSet -> true // TODO: May be add some check here.
is CharSet,
is RangeSet,
is SupplementaryCharSet,
is SupplementaryRangeSet -> false
else -> true
}
}
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
}
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Optimized greedy quantifier node ('*') for the case where there is no intersection with
* next node and normal quantifiers could be treated as greedy and possessive.
*
* @author Nikolay A. Kuznetsov
*/
// TODO: May be rename?
internal class UnifiedQuantifierSet(quant: LeafQuantifierSet) : LeafQuantifierSet(Quantifier.starQuantifier, quant.leaf, quant.next, quant.type) {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var index = startIndex
while (index + leaf.charCount <= testString.length && leaf.accepts(index, testString) > 0) {
index += leaf.charCount
}
return next.matches(index, testString, matchResult)
}
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
var startSearch = next.find(startIndex, testString, matchResult)
if (startSearch < 0)
return -1
var result = startSearch
var index = startSearch - leaf.charCount
while (index >= startIndex && leaf.accepts(index, testString) > 0) {
result = index
index -= leaf.charCount
}
return result
}
init {
innerSet.next = this // TODO: O_o
}
}
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
/**
* Represents word boundary, checks current character and previous one. If they have different types returns true;
*
* @author Nikolay A. Kuznetsov
*/
internal class WordBoundarySet(var positive: Boolean) : SimpleSet() {
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
val curChar = if (startIndex >= testString.length) ' ' else testString[startIndex]
val prevChar = if (startIndex == 0) ' ' else testString[startIndex - 1]
val right = curChar == ' ' || isSpace(curChar, startIndex, testString)
val left = prevChar == ' ' || isSpace(prevChar, startIndex - 1, testString)
return if (left xor right xor positive)
-1
else
next.matches(startIndex, testString, matchResult)
}
/** Returns false, because word boundary does not consumes any characters and do not move string index. */
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
override val name: String
get() = "WordBoundarySet"
private fun isSpace(char: Char, startIndex: Int, testString: CharSequence): Boolean {
if (char.isLetterOrDigit() || char == '_') {
return false
}
if (char.category == CharCategory.NON_SPACING_MARK) {
var index = startIndex
while (--index >= 0) {
val ch = testString[index]
when {
ch.isLetterOrDigit() -> return false
char.category != CharCategory.NON_SPACING_MARK -> return true
}
}
}
return true
}
}