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