# Parse Run Length Encoded file for cellular automaton data

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("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
['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.
"""
def __init__(self, rle_string):
self.rle_string = rle_string
self.name = ""
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 ")
elif line.startswith("#C") or line.startswith("#c"):
# 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  ## 2 Answers In Python using getters when not necessary is frowned upon. If you really need them, use a property: class A: def __init__(self, x): self.l = x # attribute can be accessed directly self._api_code = "XXX" # attribute will be accessed as property @property def size(self): return len(self.l) @property def api_code(self): return self._api_code >>> a = A([1,2,3]) >>> a.size 3 >>> a.l [1, 2, 3]  This would also make you main a lot easier: 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: {self.name} comments: {self.comments} author: {self.author} size_x: {self.size_x} size_y: {self.size_y} rule_birth: {self.rule_birth} rule_survival: {self.rule_survivor} pattern_raw: {self.pattern_raw} human_friendly_pattern: {self.human_friendly_pattern}""".format(self=rle_parser))  For most of these, you don't need to change anything. Only self.comments and self.human_friendly_pattern would need a definition using @property, for the rest, direct attribute access is sufficient. And actually, you can now move this whole string into RunLengthEncodedParser.__str__, then you can just do print(rle_parser). If you want to keep that one as just the string, override RunLengthEncodedParser.__format__ Final code: import pprint 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 = [] # Note underscore 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): ... def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'): ... def isdigit(self, c): """Returns True is the character is a digit""" return '0' <= c <= '9' def __str__(self): return self.rle_string def __format__(self, fmt): return """name: {self.name} comments: {self.comments} author: {self.author} size_x: {self.size_x} size_y: {self.size_y} rule_birth: {self.rule_birth} rule_survival: {self.rule_survivor} pattern_raw: {self.pattern_raw} human_friendly_pattern: {self.human_friendly_pattern}""".format(self=self) @property def 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 @property def comments(self): return pprint.pformat(self._comments) 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(format(rle_parser))  In Python 3.6, this gets slightly easier, because you can replace """...{self.author}...""".format(self=self) with f-strings using f"""...{self.author}...""". Apart from @Graipher's great answer (my answer is based on his), I'd use the following when it comes to multi-line styling: from pprint import pformat 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 = [] # Note underscore 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): ... def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'): ... def isdigit(self, c): """Returns True is the character is a digit""" return '0' <= c <= '9' def __str__(self): return self.rle_string def __format__(self, fmt): return 'name: {self.name}\n' \ 'comments: {self.comments}\n' \ 'author: {self.author}\n' \ 'size_x: {self.size_x}\n' \ 'size_y: {self.size_y}\n' \ 'rule_birth: {self.rule_birth}\n' \ 'rule_survival: {self.rule_survivor}\n' \ 'pattern_raw: {self.pattern_raw}\n' \ 'human_friendly_pattern: {self.human_friendly_pattern}\n'.format(self=self) @property def 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 @property def comments(self): return pformat(self._comments) def main(): sample_rle = '#N Gosper glider gun\n' \ '#C This was the first gun discovered.\n' \ '#C This was the first gun discovered.\n' \ '#C As its name suggests, it was discovered by Bill Gosper.\n' \ '#O Bill Gosper Nov. 1970\n' \ 'x = 36, y = 9, rule = B3/S23\n' \ '24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b\n' \
'obo$10bo5bo7bo$11bo3bo\$12b2o!\n'

rle_parser = RunLengthEncodedParser(sample_rle)
print(format(rle_parser))
if __name__ == '__main__':
main()


For me, the trade-off of using \n and \ to gain the correct indentation makes sense + makes those snippets look clear.

I also added the from pprint import pformat at the top of the code.