refactor: rename matchAll to findAll and update related tests for consistency

This commit is contained in:
monoid 2025-06-29 16:19:49 +09:00
parent b7b71fb09d
commit 4f7cfae72d
3 changed files with 73 additions and 97 deletions

View file

@ -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

View file

@ -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 // 두 매칭 결과를 합쳐서 반환
)
} }
} }

View file

@ -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)