From b7b71fb09db4aa2d3a58faa5626991d0549b7563 Mon Sep 17 00:00:00 2001 From: monoid Date: Sun, 29 Jun 2025 15:54:00 +0900 Subject: [PATCH] refactor: simplify AvailableState handling and improve matchAll logic in RegexItem --- lib/src/main/kotlin/org/example/RegexItem.kt | 162 +++++++------------ 1 file changed, 59 insertions(+), 103 deletions(-) diff --git a/lib/src/main/kotlin/org/example/RegexItem.kt b/lib/src/main/kotlin/org/example/RegexItem.kt index fc98960..d8240b4 100644 --- a/lib/src/main/kotlin/org/example/RegexItem.kt +++ b/lib/src/main/kotlin/org/example/RegexItem.kt @@ -12,13 +12,10 @@ data class State( get() = input.substring(endIndex) } -data class AvailableState(val seq: Sequence = emptySequence()) : Sequence by seq { - val isEmpty: Boolean - get() = seq.none() -} - +typealias AvailableState = Sequence // 재귀 하향 분석기. +// 백트랙킹 기반. interface RegexItem { override fun toString(): String fun findMatch(str: String, position: Int = 0): AvailableState @@ -26,27 +23,26 @@ interface RegexItem { fun RegexItem.matchAll(item: String): Sequence { return sequence { - var position = 0; + var position = 0 // 문자열의 끝까지 반복합니다. 비어있어도 한번은 시도합니다. while (position <= item.length) { // findMatch 메서드를 호출하여 매칭을 시도합니다. - val matchResult = findMatch(item, position) - if (matchResult.isEmpty) { + val matchResult = findMatch(item, position).firstOrNull() + if (matchResult === null) { // 매칭이 실패하면 position을 증가시키고 다시 시도합니다. - if (position < item.length) { - position++ - continue - } else { - // position이 문자열의 끝에 도달했지만 매칭이 실패한 경우 종료 - break - } - } else { - // 매칭이 성공하면 MatchResult를 생성하여 반환합니다. - val state = matchResult.seq.first() - yield(state) - // 다음 위치로 이동합니다. - position = state.endIndex + position++ + continue } + // 매칭이 성공하면 MatchResult를 생성하여 반환합니다. + yield(matchResult) + + // 다음 위치로 이동합니다. + position = + if (matchResult.startIndex == matchResult.endIndex) { + position + 1 + } else { + matchResult.endIndex + } } } } @@ -65,31 +61,17 @@ class AndThenItem(val left: RegexItem, val right: RegexItem) : RegexItem { override fun toString(): String = "${left}${right}" override fun findMatch(str: String, position: Int): AvailableState { val leftMatch = left.findMatch(str, position) - if (leftMatch.isEmpty) { - return AvailableState() // If left match fails, return empty sequence + return leftMatch.flatMap { leftState -> + 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 + ) + } } - // If left match is successful, try to match the right item with the remaining string - // from the left match. - - return AvailableState( - leftMatch.flatMap { leftState -> - val rightMatch = right.findMatch(str, leftState.endIndex) - if (!rightMatch.isEmpty) { - // If right match is successful, combine the matched parts - rightMatch.map { rightState -> - State( - str, - leftState.startIndex, - rightState.endIndex, - leftState.captures + rightState.captures - ) // Combine captures - } - } else { - // If right match fails, return an empty sequence - emptySequence() - } - } - ) } } @@ -113,9 +95,9 @@ class CharItem(val value: String) : RegexItem { return when { // 첫번째 문자가 value와 일치하는지 확인 position < str.length && str[position].toString() == value -> { - AvailableState(sequenceOf(State(str, position, position + 1))) + (sequenceOf(State(str, position, position + 1))) } - else -> AvailableState() + else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환 } } } @@ -128,9 +110,9 @@ class BracketItem(val content: String) : RegexItem { // 대괄호 안의 내용과 일치하는 첫 문자를 찾음 return when { position < str.length && content.contains(str[position]) -> { - AvailableState(sequenceOf(State(str, position, position + 1))) + (sequenceOf(State(str, position, position + 1))) } - else -> AvailableState() + else -> emptySequence() // 일치하지 않으면 빈 시퀀스 반환 } } } @@ -142,11 +124,7 @@ class GroupItem(val item: RegexItem, val name: String) : RegexItem { // 그룹은 내부 아이템과 동일하게 매칭을 시도 val ret = item.findMatch(str, position) // 매칭된 상태에 그룹 이름을 추가하여 반환 - return AvailableState( - ret.seq.map { state -> - state.copy(captures = state.captures + (name to state.matched)) - } - ) + return ret.map { state -> state.copy(captures = state.captures + (name to state.matched)) } } } @@ -156,29 +134,27 @@ class AnchorItem(val anchor: String) : RegexItem { override fun findMatch(str: String, position: Int): AvailableState { // 앵커는 문자열의 시작(^) 또는 끝($)과 매칭됨 return when (anchor) { - "^" -> if (position == 0) { - AvailableState(sequenceOf(State(str, 0, 0))) - } else { - AvailableState() // 시작 앵커가 실패하면 빈 시퀀스 반환 - } - "$" -> if (position == str.length) { - AvailableState(sequenceOf(State(str, str.length, str.length))) - } else { - AvailableState() // 끝 앵커가 실패하면 빈 시퀀스 반환 - } + "^" -> + if (position == 0) { + (sequenceOf(State(str, 0, 0))) + } else { + emptySequence() // 시작 앵커가 실패하면 빈 시퀀스 반환 + } + "$" -> + if (position == str.length) { + (sequenceOf(State(str, str.length, str.length))) + } else { + emptySequence() // 끝 앵커가 실패하면 빈 시퀀스 반환 + } // 다른 앵커는 지원하지 않음 else -> throw IllegalArgumentException("Unknown anchor: $anchor") } } } -fun matchMany( - str: String, - item: RegexItem, - position: Int -): Sequence { +fun matchMany(str: String, item: RegexItem, position: Int): Sequence { // 욕심쟁이 매칭을 위한 헬퍼 함수 - return item.findMatch(str, position).seq.flatMap { state -> + return item.findMatch(str, position).flatMap { state -> if (state.endIndex == str.length) { sequenceOf(state) // If remaining is empty, return the matched state } else { @@ -190,26 +166,10 @@ fun matchMany( } } -// fun matchMany(str: String, item: RegexItem): Sequence = sequence { -// val stack = mutableListOf(item.findMatch(str).seq) -// while (stack.isNotEmpty()) { -// val current = stack.removeAt(stack.lastIndex) -// for (state in current) { -// yield(state) // Yield the current state -// if (state.remaining.isNotEmpty()) { -// // If there is remaining string, continue matching -// stack.add(item.findMatch(state.remaining).seq.map { nextState -> -// State(state.matched + nextState.matched, nextState.remaining) -// }) -// } -// } -// } -// } - class PlusItem(val item: RegexItem) : RegexItem { override fun toString(): String = "${item}+" override fun findMatch(str: String, position: Int): AvailableState { - return AvailableState(matchMany(str, item, position)) + return (matchMany(str, item, position)) } } @@ -218,16 +178,14 @@ class StarItem(val item: RegexItem) : RegexItem { override fun findMatch(str: String, position: Int): AvailableState { // *는 0개 이상의 매칭을 의미하므로, 먼저 시도해보고 실패하면 빈 시퀀스를 반환 val matchResult = this.item.findMatch(str, position) - if (matchResult.isEmpty) { - // If the item does not match, return an empty sequence - return AvailableState(sequenceOf(State(str, position, 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 AvailableState( - matchResult.flatMap { state -> - sequenceOf(state) + matchMany(str, this.item, state.endIndex) - } - ) + return (matchResult.flatMap { state -> + matchMany(str, this.item, state.endIndex) + sequenceOf(state) + }) } } @@ -236,12 +194,12 @@ class QuestionItem(val item: RegexItem) : RegexItem { override fun findMatch(str: String, position: Int): AvailableState { // ?는 0개 또는 1개 매칭을 의미하므로, 먼저 시도해보고 실패하면 빈 시퀀스를 반환 val matchResult = this.item.findMatch(str, position) - if (matchResult.isEmpty) { + if (matchResult.none()) { // If the item does not match, return an empty sequence - return AvailableState(sequenceOf(State(str, position, position))) + return (sequenceOf(State(str, position, position))) } // If it matches, return the successful match - return AvailableState(matchResult.map { State(str, it.startIndex, it.endIndex) }) + return (matchResult.map { State(str, it.startIndex, it.endIndex) }) } } @@ -250,9 +208,8 @@ class DotItem : RegexItem { override fun findMatch(str: String, position: Int): AvailableState = // .은 임의의 한 문자와 매칭되므로, 첫 문자가 존재하면 매칭 성공 when { - position < str.length -> - AvailableState(sequenceOf(State(str, position, position + 1))) - else -> AvailableState() // 빈 문자열에 대해서는 매칭 실패 + position < str.length -> (sequenceOf(State(str, position, position + 1))) + else -> emptySequence() // 빈 문자열에 대해서는 매칭 실패 } } @@ -263,8 +220,7 @@ class AlternationItem(val left: RegexItem, val right: RegexItem) : RegexItem { val leftMatch = left.findMatch(str, position) val rightMatch = right.findMatch(str, position) - return AvailableState( - (leftMatch + rightMatch) // 두 매칭 결과를 합쳐서 반환 + return ((leftMatch + rightMatch) // 두 매칭 결과를 합쳐서 반환 ) } }