238 lines
6.0 KiB
Python
238 lines
6.0 KiB
Python
# https://www.mycompiler.io/view/3TFZagC
|
|
|
|
class ParseError(Exception):
|
|
def __init__(self, pos, msg, *args):
|
|
self.pos = pos
|
|
self.msg = msg
|
|
self.args = args
|
|
|
|
def __str__(self):
|
|
return '%s at position %s' % (self.msg % self.args, self.pos)
|
|
|
|
class Parser:
|
|
def __init__(self):
|
|
self.cache = {}
|
|
|
|
def parse(self, text):
|
|
self.text = text
|
|
self.pos = -1
|
|
self.len = len(text) - 1
|
|
rv = self.start()
|
|
self.assert_end()
|
|
return rv
|
|
|
|
def assert_end(self):
|
|
if self.pos < self.len:
|
|
raise ParseError(
|
|
self.pos + 1,
|
|
'Expected end of string but got %s',
|
|
self.text[self.pos + 1]
|
|
)
|
|
|
|
def eat_whitespace(self):
|
|
while self.pos < self.len and self.text[self.pos + 1] in " \f\v\r\t\n":
|
|
self.pos += 1
|
|
|
|
def split_char_ranges(self, chars):
|
|
try:
|
|
return self.cache[chars]
|
|
except KeyError:
|
|
pass
|
|
|
|
rv = []
|
|
index = 0
|
|
length = len(chars)
|
|
|
|
while index < length:
|
|
if index + 2 < length and chars[index + 1] == '-':
|
|
if chars[index] >= chars[index + 2]:
|
|
raise ValueError('Bad character range')
|
|
|
|
rv.append(chars[index:index + 3])
|
|
index += 3
|
|
else:
|
|
rv.append(chars[index])
|
|
index += 1
|
|
|
|
self.cache[chars] = rv
|
|
return rv
|
|
|
|
def char(self, chars=None):
|
|
if self.pos >= self.len:
|
|
raise ParseError(
|
|
self.pos + 1,
|
|
'Expected %s but got end of string',
|
|
'character' if chars is None else '[%s]' % chars
|
|
)
|
|
|
|
next_char = self.text[self.pos + 1]
|
|
if chars == None:
|
|
self.pos += 1
|
|
return next_char
|
|
|
|
for char_range in self.split_char_ranges(chars):
|
|
if len(char_range) == 1:
|
|
if next_char == char_range:
|
|
self.pos += 1
|
|
return next_char
|
|
elif char_range[0] <= next_char <= char_range[2]:
|
|
self.pos += 1
|
|
return next_char
|
|
|
|
raise ParseError(
|
|
self.pos + 1,
|
|
'Expected %s but got %s',
|
|
'character' if chars is None else '[%s]' % chars,
|
|
next_char
|
|
)
|
|
|
|
def keyword(self, *keywords):
|
|
self.eat_whitespace()
|
|
if self.pos >= self.len:
|
|
raise ParseError(
|
|
self.pos + 1,
|
|
'Expected %s but got end of string',
|
|
','.join(keywords)
|
|
)
|
|
|
|
for keyword in keywords:
|
|
low = self.pos + 1
|
|
high = low + len(keyword)
|
|
|
|
if self.text[low:high] == keyword:
|
|
self.pos += len(keyword)
|
|
self.eat_whitespace()
|
|
return keyword
|
|
|
|
raise ParseError(
|
|
self.pos + 1,
|
|
'Expected %s but got %s',
|
|
','.join(keywords),
|
|
self.text[self.pos + 1],
|
|
)
|
|
|
|
def match(self, *rules):
|
|
self.eat_whitespace()
|
|
last_error_pos = -1
|
|
last_exception = None
|
|
last_error_rules = []
|
|
|
|
for rule in rules:
|
|
initial_pos = self.pos
|
|
try:
|
|
rv = getattr(self, rule)()
|
|
self.eat_whitespace()
|
|
return rv
|
|
except ParseError as e:
|
|
self.pos = initial_pos
|
|
|
|
if e.pos > last_error_pos:
|
|
last_exception = e
|
|
last_error_pos = e.pos
|
|
last_error_rules.clear()
|
|
last_error_rules.append(rule)
|
|
elif e.pos == last_error_pos:
|
|
last_error_rules.append(rule)
|
|
|
|
if len(last_error_rules) == 1:
|
|
raise last_exception
|
|
else:
|
|
raise ParseError(
|
|
last_error_pos,
|
|
'Expected %s but got %s',
|
|
','.join(last_error_rules),
|
|
self.text[last_error_pos]
|
|
)
|
|
|
|
def maybe_char(self, chars=None):
|
|
try:
|
|
return self.char(chars)
|
|
except ParseError:
|
|
return None
|
|
|
|
def maybe_match(self, *rules):
|
|
try:
|
|
return self.match(*rules)
|
|
except ParseError:
|
|
return None
|
|
|
|
def maybe_keyword(self, *keywords):
|
|
try:
|
|
return self.keyword(*keywords)
|
|
except ParseError:
|
|
return None
|
|
|
|
class CalcParser(Parser):
|
|
def start(self):
|
|
return self.expression()
|
|
|
|
def expression(self):
|
|
rv = self.match('term')
|
|
while True:
|
|
op = self.maybe_keyword('+', '-')
|
|
if op is None:
|
|
break
|
|
|
|
term = self.match('term')
|
|
if op == '+':
|
|
rv += term
|
|
else:
|
|
rv -= term
|
|
|
|
return rv
|
|
|
|
def term(self):
|
|
rv = self.match('factor')
|
|
while True:
|
|
op = self.maybe_keyword('*', '/')
|
|
if op is None:
|
|
break
|
|
|
|
term = self.match('factor')
|
|
if op == '*':
|
|
rv *= term
|
|
else:
|
|
rv /= term
|
|
|
|
return rv
|
|
|
|
def factor(self):
|
|
if self.maybe_keyword('('):
|
|
rv = self.match('expression')
|
|
self.keyword(')')
|
|
|
|
return rv
|
|
|
|
return self.match('number')
|
|
|
|
def number(self):
|
|
chars = []
|
|
|
|
sign = self.maybe_keyword('+', '-')
|
|
if sign is not None:
|
|
chars.append(sign)
|
|
|
|
chars.append(self.char('0-9'))
|
|
|
|
while True:
|
|
char = self.maybe_char('0-9')
|
|
if char is None:
|
|
break
|
|
|
|
chars.append(char)
|
|
|
|
if self.maybe_char('.'):
|
|
chars.append('.')
|
|
chars.append(self.char('0-9'))
|
|
|
|
while True:
|
|
char = self.maybe_char('0-9')
|
|
if char is None:
|
|
break
|
|
|
|
chars.append(char)
|
|
|
|
rv = float(''.join(chars))
|
|
return rv
|
|
|