I wanted to be able to parse Run Length Encoded (*.rle)
files into something usable for a cellular automaton simulator (e.g. Conway's Game of Life and such).
Here is my main
method which has the code I used for manual testing. The example file is taken from the above linked wiki.
def main():
sample_rle = \
"""#N Gosper glider gun
#C This was the first gun discovered.
#C As its name suggests, it was discovered by Bill Gosper.
#O Bill Gosper Nov. 1970
x = 36, y = 9, rule = B3/S23
24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o! """
rle_parser = RunLengthEncodedParser(sample_rle)
print("name:", rle_parser.get_name())
print("comments:")
pprint.pprint(rle_parser.get_comments())
print("author:", rle_parser.get_author())
print("size_x:", rle_parser.get_size_x())
print("size_y:", rle_parser.get_size_y())
print("rule_birth:", rle_parser.get_rule_birth())
print("rule_survival:", rle_parser.get_rule_survival())
print("pattern_raw:", rle_parser.get_pattern_raw())
#print("pattern_2d_array:")
#print(rle_parser.get_pattern_2d_array())
print("human_friendly_pattern:")
print(rle_parser.get_human_friendly_pattern())
The console output of the file is as follows. I left out the output from print(rle_parser.get_pattern_2d_array())
as it is just a 2D list of b
(dead) and o
(alive) cells. For the sake of human readability, I replaced b
dead cells with dots in the return from get_human_friendly_pattern
.
name: Gosper glider gun comments: ['This was the first gun discovered.', 'As its name suggests, it was discovered by Bill Gosper.'] author: Bill Gosper Nov. 1970 size_x: 36 size_y: 9 rule_birth: [3] rule_survival: [2, 3] pattern_raw: 24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4bobo$10bo5bo7bo$11bo3bo$12b2o! human_friendly_pattern: ........................o........... ......................o.o........... ............oo......oo............oo ...........o...o....oo............oo oo........o.....o...oo.............. oo........o...o.oo....o.o........... ..........o.....o.......o........... ...........o...o.................... ............oo......................
I used an OOP approach with the intention of only having to parse the file once, getting all the data from it. The above example is tiny, some of these files can contain thousands of rows and columns.
It works fine, but some parts of the populate_attributes
method are nested up to 7 deep, and it feels like there should be some ways to improve its performance. Then again, the most deeply nested portions really only parse one line of characters, so I don't suspect the performance would suffer that much from it...
On another note, should I keep all those getters? Being that many of them just return an instance attribute, I thought perhaps just using the attributes in the calling code would be fine, but I don't know if that would be ideal. Not that anyone could stop someone from doing so anyways in Python, but if I plan on adapting this in other languages that support private attributes, would using getters like this be a sensible approach?
I'd like to hear any other improvements as well, from readability to structure to naming. Here is a working version on repl.it, and the RunLengthEncodedParser
class below.
class RunLengthEncodedParser:
"""
Parser for Run Length Encode (RLE) strings / files.
More information: http://www.conwaylife.com/w/index.php?title=Run_Length_Encoded
"""
def __init__(self, rle_string):
self.rle_string = rle_string
self.name = ""
self.comments = []
self.author = ""
self.size_x = 0
self.size_y = 0
self.rule_birth = []
self.rule_survival = []
self.pattern_raw = ""
# Fill in instance attributes by parsing the raw strings
self.populate_attributes(self.rle_string.strip().splitlines())
self.pattern_2d_array = self.populate_pattern(self.pattern_raw, self.size_x, self.size_y)
def populate_attributes(self, lines):
"""
This method performs all the string parsing required to parse the various
fields of data into their respective data members.
"""
for line in lines:
# Name of the pattern
if line.startswith("#N"):
self.name = line.lstrip("#N ")
# Comments accompanying the pattern
elif line.startswith("#C") or line.startswith("#c"):
self.comments.append(line.lstrip("#Cc "))
# Authorship of the pattern
elif line.startswith("#O"):
self.author = line.lstrip("#O ")
# Grid sizes and rules
elif line.startswith("x"):
data = line.split(",")
for d in data:
# Grid sizes
if d.strip().startswith("x"):
_, x = d.split("=")
self.size_x = int(x.strip())
elif d.strip().startswith("y"):
_, y = d.split("=")
self.size_y = int(y.strip())
# Rules
elif d.strip().startswith("rule"):
_, rule = d.split("=")
for r in rule.strip().split("/"):
if r.startswith("B"):
for digit in list(r.lstrip("B")):
self.rule_birth.append(int(digit))
if r.startswith("S"):
for digit in list(r.lstrip("S")):
self.rule_survival.append(int(digit))
# Other lines should contain the actual pattern
else:
self.pattern_raw += line.strip(" \n\r\t")
def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'):
pattern = []
pattern_rows = pattern_raw.rstrip("!").split("$")
assert len(pattern_rows) == size_y, \
"Number of data rows {0} does not match size y = {1}".format(len(pattern_rows), size_y)
for y in range(size_y):
pattern.append([])
tmp_num_str = ""
for c in pattern_rows[y]:
if self.isdigit(c):
tmp_num_str += c
else:
if tmp_num_str == "":
num_cells = 1
else:
num_cells = int(tmp_num_str)
for n in range(num_cells):
pattern[y].append(c)
#reset count until another number is encountered
tmp_num_str = ""
#fill in empty spaces at end of each row
for _ in range(len(pattern[y]), size_x):
pattern[y].append(default_cell)
return pattern
def isdigit(self, c):
"""Returns True is the character is a digit"""
return '0' <= c <= '9'
def __str__(self):
return self.rle_string
# Getters
def get_name(self):
return self.name
def get_comments(self):
return self.comments
def get_author(self):
return self.author
def get_size_x(self):
return self.size_x
def get_size_y(self):
return self.size_y
def get_rule_birth(self):
return self.rule_birth
def get_rule_survival(self):
return self.rule_survival
def get_pattern_raw(self):
return self.pattern_raw
def get_pattern_2d_array(self):
return self.pattern_2d_array
def get_human_friendly_pattern(self):
pattern_str = ""
for row in self.pattern_2d_array:
row_str = ""
for c in row:
if c == 'b':
row_str += '.'
else:
row_str += c
pattern_str += row_str + '\n'
return pattern_str