refactor: rename matchAll to findAll and update related tests for consistency
This commit is contained in:
parent
b7b71fb09d
commit
4f7cfae72d
3 changed files with 73 additions and 97 deletions
|
@ -12,7 +12,6 @@ import com.github.h0tk3y.betterParse.parser.Parser
|
||||||
class RegexParser : Grammar<RegexItem>() {
|
class RegexParser : Grammar<RegexItem>() {
|
||||||
private var groupCounter = 0
|
private var groupCounter = 0
|
||||||
|
|
||||||
// val bracketContent by regexToken("[^\\]]*")
|
|
||||||
val escapedCharacter by regexToken("\\\\[+*?.$^()|\\[\\]]")
|
val escapedCharacter by regexToken("\\\\[+*?.$^()|\\[\\]]")
|
||||||
val postfixOperator by regexToken("[+*?]")
|
val postfixOperator by regexToken("[+*?]")
|
||||||
val anchorOperator by regexToken("[$^]")
|
val anchorOperator by regexToken("[$^]")
|
||||||
|
@ -63,20 +62,14 @@ class RegexParser : Grammar<RegexItem>() {
|
||||||
})
|
})
|
||||||
|
|
||||||
val term: Parser<RegexItem> by
|
val term: Parser<RegexItem> by
|
||||||
(item and optional(postfixOperator)) map
|
(item and optional(postfixOperator)) map { (item, op) ->
|
||||||
{ result ->
|
when (op?.text) {
|
||||||
result.t1.let { first ->
|
"+" -> PlusItem(item)
|
||||||
result.t2?.let {
|
"*" -> StarItem(item)
|
||||||
when (it.text) {
|
"?" -> QuestionItem(item)
|
||||||
"+" -> PlusItem(first)
|
else -> item
|
||||||
"*" -> StarItem(first)
|
}
|
||||||
"?" -> QuestionItem(first)
|
}
|
||||||
else -> first
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?: first
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val andThen: Parser<RegexItem> by
|
val andThen: Parser<RegexItem> by
|
||||||
oneOrMore(term) map { items -> items.reduce { left, right -> AndThenItem(left, right) } }
|
oneOrMore(term) map { items -> items.reduce { left, right -> AndThenItem(left, right) } }
|
||||||
val termWithAlternation: Parser<RegexItem> by
|
val termWithAlternation: Parser<RegexItem> by
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package org.example
|
package org.example
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
val input: String,
|
val input: String,
|
||||||
val startIndex: Int,
|
val startIndex: Int,
|
||||||
val endIndex: Int,
|
val endIndex: Int,
|
||||||
val captures: Map<String, String> = emptyMap()
|
val captures: Map<String, String> = emptyMap()
|
||||||
) {
|
) {
|
||||||
val matched: String
|
val matched: String
|
||||||
get() = input.substring(startIndex, endIndex)
|
get() = input.substring(startIndex, endIndex)
|
||||||
|
@ -21,14 +21,14 @@ interface RegexItem {
|
||||||
fun findMatch(str: String, position: Int = 0): AvailableState
|
fun findMatch(str: String, position: Int = 0): AvailableState
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RegexItem.matchAll(item: String): Sequence<State> {
|
fun RegexItem.findAll(item: String): Sequence<State> {
|
||||||
return sequence {
|
return sequence {
|
||||||
var position = 0
|
var position = 0
|
||||||
// 문자열의 끝까지 반복합니다. 비어있어도 한번은 시도합니다.
|
// 문자열의 끝까지 반복합니다. 비어있어도 한번은 시도합니다.
|
||||||
while (position <= item.length) {
|
while (position <= item.length) {
|
||||||
// findMatch 메서드를 호출하여 매칭을 시도합니다.
|
// findMatch 메서드를 호출하여 매칭을 시도합니다.
|
||||||
val matchResult = findMatch(item, position).firstOrNull()
|
val matchResult = findMatch(item, position).firstOrNull()
|
||||||
if (matchResult === null) {
|
if (matchResult == null) {
|
||||||
// 매칭이 실패하면 position을 증가시키고 다시 시도합니다.
|
// 매칭이 실패하면 position을 증가시키고 다시 시도합니다.
|
||||||
position++
|
position++
|
||||||
continue
|
continue
|
||||||
|
@ -38,23 +38,23 @@ fun RegexItem.matchAll(item: String): Sequence<State> {
|
||||||
|
|
||||||
// 다음 위치로 이동합니다.
|
// 다음 위치로 이동합니다.
|
||||||
position =
|
position =
|
||||||
if (matchResult.startIndex == matchResult.endIndex) {
|
if (matchResult.startIndex == matchResult.endIndex) {
|
||||||
position + 1
|
position + 1
|
||||||
} else {
|
} else {
|
||||||
matchResult.endIndex
|
matchResult.endIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RegexItem.match(item: String): State? {
|
fun RegexItem.find(item: String): State? {
|
||||||
// matchAll 에서 첫 번째 매칭 결과를 반환합니다.
|
// findAll 에서 첫 번째 매칭 결과를 반환합니다.
|
||||||
return this.matchAll(item).firstOrNull()
|
return this.findAll(item).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RegexItem.test(item: String): Boolean {
|
fun RegexItem.containsMatchIn(item: String): Boolean {
|
||||||
// 매칭 결과가 성공인지 확인하는 헬퍼 함수
|
// 매칭 결과가 성공인지 확인하는 헬퍼 함수
|
||||||
return this.match(item) != null
|
return this.find(item) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
class AndThenItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
class AndThenItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
||||||
|
@ -65,10 +65,10 @@ class AndThenItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
||||||
right.findMatch(str, leftState.endIndex).map { rightState ->
|
right.findMatch(str, leftState.endIndex).map { rightState ->
|
||||||
// If right match is successful, combine the matched parts
|
// If right match is successful, combine the matched parts
|
||||||
State(
|
State(
|
||||||
str,
|
str,
|
||||||
leftState.startIndex,
|
leftState.startIndex,
|
||||||
rightState.endIndex,
|
rightState.endIndex,
|
||||||
leftState.captures + rightState.captures // Combine captures
|
leftState.captures + rightState.captures // Combine captures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,25 +77,17 @@ class AndThenItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
||||||
|
|
||||||
class CharItem(val value: String) : RegexItem {
|
class CharItem(val value: String) : RegexItem {
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
// escape 특수 문자를 처리하여 출력
|
// escape 특수 문자를 처리하여 출력
|
||||||
when (value) {
|
when (value) {
|
||||||
"+" -> "\\+"
|
"\\", "+", "*", "?", ".", "(", ")", "|", "[", "]" -> "\\$value"
|
||||||
"*" -> "\\*"
|
else -> value // 일반 문자 그대로 반환
|
||||||
"?" -> "\\?"
|
}
|
||||||
"." -> "\\."
|
|
||||||
"(" -> "\\("
|
|
||||||
")" -> "\\)"
|
|
||||||
"|" -> "\\|"
|
|
||||||
"[" -> "\\["
|
|
||||||
"]" -> "\\]"
|
|
||||||
else -> value // 일반 문자 그대로 반환
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findMatch(str: String, position: Int): AvailableState {
|
override fun findMatch(str: String, position: Int): AvailableState {
|
||||||
return when {
|
return when {
|
||||||
// 첫번째 문자가 value와 일치하는지 확인
|
// 첫번째 문자가 value와 일치하는지 확인
|
||||||
position < str.length && str[position].toString() == value -> {
|
position < str.length && str[position].toString() == value -> {
|
||||||
(sequenceOf(State(str, position, position + 1)))
|
sequenceOf(State(str, position, position + 1))
|
||||||
}
|
}
|
||||||
else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환
|
else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환
|
||||||
}
|
}
|
||||||
|
@ -110,7 +102,7 @@ class BracketItem(val content: String) : RegexItem {
|
||||||
// 대괄호 안의 내용과 일치하는 첫 문자를 찾음
|
// 대괄호 안의 내용과 일치하는 첫 문자를 찾음
|
||||||
return when {
|
return when {
|
||||||
position < str.length && content.contains(str[position]) -> {
|
position < str.length && content.contains(str[position]) -> {
|
||||||
(sequenceOf(State(str, position, position + 1)))
|
sequenceOf(State(str, position, position + 1))
|
||||||
}
|
}
|
||||||
else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환
|
else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환
|
||||||
}
|
}
|
||||||
|
@ -135,17 +127,17 @@ class AnchorItem(val anchor: String) : RegexItem {
|
||||||
// 앵커는 문자열의 시작(^) 또는 끝($)과 매칭됨
|
// 앵커는 문자열의 시작(^) 또는 끝($)과 매칭됨
|
||||||
return when (anchor) {
|
return when (anchor) {
|
||||||
"^" ->
|
"^" ->
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
(sequenceOf(State(str, 0, 0)))
|
sequenceOf(State(str, 0, 0))
|
||||||
} else {
|
} else {
|
||||||
emptySequence() // 시작 앵커가 실패하면 빈 시퀀스 반환
|
emptySequence() // 시작 앵커가 실패하면 빈 시퀀스 반환
|
||||||
}
|
}
|
||||||
"$" ->
|
"$" ->
|
||||||
if (position == str.length) {
|
if (position == str.length) {
|
||||||
(sequenceOf(State(str, str.length, str.length)))
|
sequenceOf(State(str, str.length, str.length))
|
||||||
} else {
|
} else {
|
||||||
emptySequence() // 끝 앵커가 실패하면 빈 시퀀스 반환
|
emptySequence() // 끝 앵커가 실패하면 빈 시퀀스 반환
|
||||||
}
|
}
|
||||||
// 다른 앵커는 지원하지 않음
|
// 다른 앵커는 지원하지 않음
|
||||||
else -> throw IllegalArgumentException("Unknown anchor: $anchor")
|
else -> throw IllegalArgumentException("Unknown anchor: $anchor")
|
||||||
}
|
}
|
||||||
|
@ -169,48 +161,40 @@ fun matchMany(str: String, item: RegexItem, position: Int): Sequence<State> {
|
||||||
class PlusItem(val item: RegexItem) : RegexItem {
|
class PlusItem(val item: RegexItem) : RegexItem {
|
||||||
override fun toString(): String = "${item}+"
|
override fun toString(): String = "${item}+"
|
||||||
override fun findMatch(str: String, position: Int): AvailableState {
|
override fun findMatch(str: String, position: Int): AvailableState {
|
||||||
return (matchMany(str, item, position))
|
return matchMany(str, item, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StarItem(val item: RegexItem) : RegexItem {
|
class StarItem(val item: RegexItem) : RegexItem {
|
||||||
override fun toString(): String = "${item}*"
|
override fun toString(): String = "${item}*"
|
||||||
override fun findMatch(str: String, position: Int): AvailableState {
|
override fun findMatch(str: String, position: Int): AvailableState {
|
||||||
// *는 0개 이상의 매칭을 의미하므로, 먼저 시도해보고 실패하면 빈 시퀀스를 반환
|
// *는 0번 또는 1번 이상 일치합니다.
|
||||||
val matchResult = this.item.findMatch(str, position)
|
// 욕심쟁이(greedy) 방식으로 구현하기 위해, 가장 긴 매치(1번 이상)를 먼저 찾고, 그 다음에 0번 매치를 추가합니다.
|
||||||
if (matchResult.none()) {
|
val oneOrMoreMatches = matchMany(str, item, position)
|
||||||
// 아이템이 매칭되지 않으면 빈 시퀀스를 반환
|
val zeroMatch = sequenceOf(State(str, position, position))
|
||||||
return (sequenceOf(State(str, position, position)))
|
return oneOrMoreMatches + zeroMatch
|
||||||
}
|
|
||||||
// If it matches, return the successful match and continue matching with the remaining string
|
|
||||||
return (matchResult.flatMap { state ->
|
|
||||||
matchMany(str, this.item, state.endIndex) + sequenceOf(state)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class QuestionItem(val item: RegexItem) : RegexItem {
|
class QuestionItem(val item: RegexItem) : RegexItem {
|
||||||
override fun toString(): String = "${item}?"
|
override fun toString(): String = "${item}?"
|
||||||
override fun findMatch(str: String, position: Int): AvailableState {
|
override fun findMatch(str: String, position: Int): AvailableState {
|
||||||
// ?는 0개 또는 1개 매칭을 의미하므로, 먼저 시도해보고 실패하면 빈 시퀀스를 반환
|
// ?는 0번 또는 1번 일치합니다.
|
||||||
val matchResult = this.item.findMatch(str, position)
|
val oneMatch = item.findMatch(str, position)
|
||||||
if (matchResult.none()) {
|
val zeroMatch = sequenceOf(State(str, position, position))
|
||||||
// If the item does not match, return an empty sequence
|
// 1번 매치를 0번 매치보다 우선합니다.
|
||||||
return (sequenceOf(State(str, position, position)))
|
return oneMatch + zeroMatch
|
||||||
}
|
|
||||||
// If it matches, return the successful match
|
|
||||||
return (matchResult.map { State(str, it.startIndex, it.endIndex) })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DotItem : RegexItem {
|
class DotItem : RegexItem {
|
||||||
override fun toString(): String = "."
|
override fun toString(): String = "."
|
||||||
override fun findMatch(str: String, position: Int): AvailableState =
|
override fun findMatch(str: String, position: Int): AvailableState =
|
||||||
// .은 임의의 한 문자와 매칭되므로, 첫 문자가 존재하면 매칭 성공
|
// .은 임의의 한 문자와 매칭되므로, 첫 문자가 존재하면 매칭 성공
|
||||||
when {
|
when {
|
||||||
position < str.length -> (sequenceOf(State(str, position, position + 1)))
|
position < str.length -> sequenceOf(State(str, position, position + 1))
|
||||||
else -> emptySequence() // 빈 문자열에 대해서는 매칭 실패
|
else -> emptySequence() // 빈 문자열에 대해서는 매칭 실패
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlternationItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
class AlternationItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
||||||
|
@ -220,7 +204,6 @@ class AlternationItem(val left: RegexItem, val right: RegexItem) : RegexItem {
|
||||||
val leftMatch = left.findMatch(str, position)
|
val leftMatch = left.findMatch(str, position)
|
||||||
val rightMatch = right.findMatch(str, position)
|
val rightMatch = right.findMatch(str, position)
|
||||||
|
|
||||||
return ((leftMatch + rightMatch) // 두 매칭 결과를 합쳐서 반환
|
return leftMatch + rightMatch // 두 매칭 결과를 합쳐서 반환
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package org.example
|
||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
private fun checkRegex(
|
private fun checkRegex(
|
||||||
pattern: String,
|
pattern: String,
|
||||||
|
@ -15,10 +16,10 @@ private fun checkRegex(
|
||||||
|
|
||||||
private class RegexTestAsserter(private val regex: RegexItem) {
|
private class RegexTestAsserter(private val regex: RegexItem) {
|
||||||
fun String.shouldMatch() {
|
fun String.shouldMatch() {
|
||||||
assert(regex.test(this)) { "Expected '$this' to match" }
|
assert(regex.containsMatchIn(this)) { "Expected '$this' to match" }
|
||||||
}
|
}
|
||||||
fun String.shouldNotMatch() {
|
fun String.shouldNotMatch() {
|
||||||
assert(!regex.test(this)) { "Expected '$this' not to match" }
|
assert(!regex.containsMatchIn(this)) { "Expected '$this' not to match" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,19 +151,18 @@ class ParserTest {
|
||||||
val input = "(a)(b)"
|
val input = "(a)(b)"
|
||||||
val result = compileRegex(input)
|
val result = compileRegex(input)
|
||||||
assertEquals("(a)(b)", result.toString())
|
assertEquals("(a)(b)", result.toString())
|
||||||
val matchResult = result.match("ab")
|
val matchResult = result.find("ab")
|
||||||
assert(matchResult != null) { "Expected match result to be non-null" }
|
assertNotNull(matchResult, "Expected match result to be non-null")
|
||||||
if (matchResult == null) return
|
|
||||||
assertEquals("ab", matchResult.matched)
|
assertEquals("ab", matchResult.matched)
|
||||||
assertEquals(2, matchResult.captures.size)
|
assertEquals(2, matchResult.captures.size)
|
||||||
assertEquals("a", matchResult.captures.get("0"))
|
assertEquals("a", matchResult.captures["0"])
|
||||||
assertEquals("b", matchResult.captures.get("1"))
|
assertEquals("b", matchResult.captures["1"])
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
fun testMatchAll() {
|
fun testMatchAll() {
|
||||||
val input = "a+"
|
val input = "a+"
|
||||||
val regex = compileRegex(input)
|
val regex = compileRegex(input)
|
||||||
val matches = regex.matchAll("aaabaaa")
|
val matches = regex.findAll("aaabaaa")
|
||||||
val results = matches.toList()
|
val results = matches.toList()
|
||||||
println("Matches found: ${results}")
|
println("Matches found: ${results}")
|
||||||
// assertEquals(2, results.size)
|
// assertEquals(2, results.size)
|
||||||
|
|
Loading…
Add table
Reference in a new issue