Disclaimer
I am more of a Java dev, so please excuse my non-pythonesque ideas.
Style review
Write code for someone else, not yourself (i.e. readable & understandable).
You have non-descriptive variable names.
i
: usually there is a better name for it, I would consider i
viable in something like for i in range
temp
: what does temp represent? Already processed characters, so maybe call it processed_chars
or something
result
, res
- almost identical, very confusing. A single variable named result
could be OK in a function, Martin Fowler uses it, although Uncle Bob despises it. You are doing parsing, so a probable alternative could be parsed
or the like.
res
: why do you have this variable in the first place? Just use a tuple deconstruction into something more meaningful:
parsed_list, new_i = parseToList(string, i+1)
I am not sure how python work, but maybe you could even replace new_i
directly with i
.
Functionality review
You never fail. Weird. Are you sure you can always parse everything successfully? Even though this is a very simple and permissive language, probably not. Edge cases:
Design review
First of all I will create a grammar. It will ease my review and it should have simplified you your implementation:
list = "[" values "]"
# maybe values could be modified to accept dangling commas if you want
values = value { "," value }
value = list | string
string = <anything except "[" "]" "," trimmed (i.e. no leadind or trailing whitespace)>
Now we have a (context-free) grammar given by pseudo-EBNF. Usually lexer and parser are separate, but we don't really need special tokens, we could just use single characters as tokens. Usually a parser accepts a stream of tokens and outputs an AST. We don't need an AST, it could be directly interpreted as python values.
An alternative to using your whole string
and i
as a cursor is to use string
as a stream of tokens, from which you take how many you want and return the rest (substring).
Now to implement a grammar, I would create a function for each non-terminal symbol (rule), f.e. parse_list() -> []
, parse_values() -> []
, parse_value()
, parse_string() -> str
. parse()
would just call parse_values()
. If you wrap these in a class. If you fail to match a symbol, you should raise an exception or let it known in your return value.
So I would suggest signatures either:
class Parser:
def parse(input: string) -> []:
self.input = input
parsed, unprocessed = self.parse_values(input)
if unprocessed:
# handle exception, maybe print
return parsed
def parse_list(cursor: int) -> []
# Parameter: cursor index in `input`
# raises exception on error
# the whole input is stored in class field
def parse_list(unprocessed: str) -> []
# Parameter: the unprocessed input
# raises exception on error
def parse_list(unprocessed: str) -> ([], str)
# Parameter: the unprocessed input
# Returns: (parsedList, new_unprocessed) on success
# (None, unprocessed) on error
# takes from unprocessed[0]
Example implementation draft:
def parse_list(unprocessed: str) -> ([], str):
matched, unprocessed = match(unprocessed, '[')
if not matched:
return None, unprocessed
values, unprocessed = parse_values()
if values == None:
return None, unprocessed
matched, unprocessed = match(unprocessed, ']')
if not matched:
return None, unprocessed
return values
def match(unprocessed: str, to_match: str) -> (bool, str):
stripped = unprocessed.lstrip()
if stripped.startswith(to_match):
return True, stripped[to_match.len:]
else:
return False, unprocessed
If you keep a note of the remaining unprocessed input or the current cursor, you could report it when finding an error (f.e. in the raised exception)