complete regex parser
This commit is contained in:
parent
1e03bb170f
commit
a621a871e6
164
src/codewars/RegExParser.py
Normal file
164
src/codewars/RegExParser.py
Normal file
@ -0,0 +1,164 @@
|
||||
class RegExp:
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
|
||||
def __repr__(self):
|
||||
args = ", ".join(map(repr, self.args))
|
||||
return f"{self.__class__.__name__}({args})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) is type(other) and self.args == other.args
|
||||
|
||||
|
||||
class Any(RegExp):
|
||||
pass
|
||||
|
||||
|
||||
class Normal(RegExp):
|
||||
pass
|
||||
|
||||
|
||||
class Or(RegExp):
|
||||
pass
|
||||
|
||||
|
||||
class Str(RegExp):
|
||||
pass
|
||||
|
||||
|
||||
class ZeroOrMore(RegExp):
|
||||
pass
|
||||
|
||||
|
||||
# Your task is to build an AST using those nodes.
|
||||
# See sample tests or test output for examples of usage.
|
||||
|
||||
|
||||
def parse_regexp(pattern: str):
|
||||
try:
|
||||
parser = RegexParser(pattern)
|
||||
result = parser.parse_regex()
|
||||
|
||||
if not parser.end():
|
||||
raise ValueError("Unexpected characters")
|
||||
|
||||
return result
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class RegexParser:
|
||||
def __init__(self, pattern):
|
||||
self.pattern = pattern
|
||||
self.pos = 0
|
||||
|
||||
# ----------------------------
|
||||
# helpers
|
||||
# ----------------------------
|
||||
|
||||
def peek(self):
|
||||
if self.pos >= len(self.pattern):
|
||||
return None
|
||||
return self.pattern[self.pos]
|
||||
|
||||
def consume(self):
|
||||
c = self.peek()
|
||||
if c is not None:
|
||||
self.pos += 1
|
||||
return c
|
||||
|
||||
def end(self):
|
||||
return self.pos >= len(self.pattern)
|
||||
|
||||
# ----------------------------
|
||||
# regex grammar
|
||||
# ----------------------------
|
||||
|
||||
def parse_regex(self):
|
||||
return self.parse_alternation()
|
||||
|
||||
def parse_alternation(self):
|
||||
left = self.parse_concatenation()
|
||||
|
||||
seen_or = False
|
||||
|
||||
while self.peek() == "|":
|
||||
if seen_or:
|
||||
raise ValueError("Only one '|' allowed per group")
|
||||
|
||||
seen_or = True
|
||||
self.consume()
|
||||
|
||||
right = self.parse_concatenation()
|
||||
left = Or(left, right)
|
||||
|
||||
return left
|
||||
|
||||
def parse_concatenation(self):
|
||||
nodes = []
|
||||
|
||||
while True:
|
||||
c = self.peek()
|
||||
|
||||
if c is None or c in "|)":
|
||||
break
|
||||
|
||||
nodes.append(self.parse_repetition())
|
||||
|
||||
if not nodes:
|
||||
return None
|
||||
|
||||
if len(nodes) == 1:
|
||||
return nodes[0]
|
||||
|
||||
return Str(nodes)
|
||||
|
||||
def parse_repetition(self):
|
||||
node = self.parse_atom()
|
||||
|
||||
while self.peek() == "*":
|
||||
self.consume()
|
||||
|
||||
if isinstance(node, ZeroOrMore):
|
||||
raise ValueError("Consecutive '*' not allowed")
|
||||
|
||||
node = ZeroOrMore(node)
|
||||
|
||||
return node
|
||||
|
||||
def parse_atom(self):
|
||||
c = self.peek()
|
||||
|
||||
if c is None:
|
||||
return None
|
||||
|
||||
if c == "(":
|
||||
self.consume()
|
||||
node = self.parse_regex()
|
||||
|
||||
if self.peek() != ")":
|
||||
raise ValueError("Unmatched '('")
|
||||
|
||||
self.consume()
|
||||
return node
|
||||
|
||||
if c == "*":
|
||||
raise ValueError("'*' cannot start an expression")
|
||||
|
||||
if c == ".":
|
||||
self.consume()
|
||||
return Any()
|
||||
|
||||
self.consume()
|
||||
return Normal(c)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_cases = [
|
||||
"(a|b)*",
|
||||
#'I%T8]dX9b=k;lm_e/4i\x0b-+pFPWq#~\\,"a5.n}(Hcs{uCz*yA`OKJwZ7V<j\n\x0cfG !L@\r2ghQr$)[ot3Rx&M'
|
||||
]
|
||||
|
||||
for test_case in test_cases:
|
||||
print("---------------------")
|
||||
print(parse_regexp(test_case))
|
||||
Loading…
x
Reference in New Issue
Block a user