Skip to content

Commit 447eb35

Browse files
authored
sgf-parsing: implement exercise (#1359)
* implement sgf-parsing * fix import statement * create entry in config.json * fix __eq__ for Python2
1 parent e9e2965 commit 447eb35

File tree

5 files changed

+341
-0
lines changed

5 files changed

+341
-0
lines changed

config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,17 @@
13051305
"lists"
13061306
]
13071307
},
1308+
{
1309+
"uuid": "0d6325d1-c0a3-456e-9a92-cea0559e82ed",
1310+
"slug": "sgf-parsing",
1311+
"core": false,
1312+
"unlocked_by": null,
1313+
"difficulty": 7,
1314+
"topics": [
1315+
"parsing",
1316+
"trees"
1317+
]
1318+
},
13081319
{
13091320
"uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd",
13101321
"slug": "accumulate",

exercises/sgf-parsing/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# SGF Parsing
2+
3+
Parsing a Smart Game Format string.
4+
5+
[SGF](https://en.wikipedia.org/wiki/Smart_Game_Format) is a standard format for
6+
storing board game files, in particular go.
7+
8+
SGF is a fairly simple format. An SGF file usually contains a single
9+
tree of nodes where each node is a property list. The property list
10+
contains key value pairs, each key can only occur once but may have
11+
multiple values.
12+
13+
An SGF file may look like this:
14+
15+
```text
16+
(;FF[4]C[root]SZ[19];B[aa];W[ab])
17+
```
18+
19+
This is a tree with three nodes:
20+
21+
- The top level node has two properties: FF\[4\] (key = "FF", value =
22+
"4") and C\[root\](key = "C", value = "root"). (FF indicates the
23+
version of SGF and C is a comment.)
24+
- The top level node has a single child which has a single property:
25+
B\[aa\]. (Black plays on the point encoded as "aa", which is the
26+
1-1 point (which is a stupid place to play)).
27+
- The B\[aa\] node has a single child which has a single property:
28+
W\[ab\].
29+
30+
As you can imagine an SGF file contains a lot of nodes with a single
31+
child, which is why there's a shorthand for it.
32+
33+
SGF can encode variations of play. Go players do a lot of backtracking
34+
in their reviews (let's try this, doesn't work, let's try that) and SGF
35+
supports variations of play sequences. For example:
36+
37+
```text
38+
(;FF[4](;B[aa];W[ab])(;B[dd];W[ee]))
39+
```
40+
41+
Here the root node has two variations. The first (which by convention
42+
indicates what's actually played) is where black plays on 1-1. Black was
43+
sent this file by his teacher who pointed out a more sensible play in
44+
the second child of the root node: `B[dd]` (4-4 point, a very standard
45+
opening to take the corner).
46+
47+
A key can have multiple values associated with it. For example:
48+
49+
```text
50+
(;FF[4];AB[aa][ab][ba])
51+
```
52+
53+
Here `AB` (add black) is used to add three black stones to the board.
54+
55+
There are a few more complexities to SGF (and parsing in general), which
56+
you can mostly ignore. You should assume that the input is encoded in
57+
UTF-8, the tests won't contain a charset property, so don't worry about
58+
that. Furthermore you may assume that all newlines are unix style (`\n`,
59+
no `\r` or `\r\n` will be in the tests) and that no optional whitespace
60+
between properties, nodes, etc will be in the tests.
61+
62+
The exercise will have you parse an SGF string and return a tree
63+
structure of properties. You do not need to encode knowledge about the
64+
data types of properties, just use the rules for the
65+
[text](http://www.red-bean.com/sgf/sgf4.html#text) type everywhere.
66+
67+
## Exception messages
68+
69+
Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to
70+
indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not
71+
every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
72+
a message.
73+
74+
To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
75+
`raise Exception`, you should write:
76+
77+
```python
78+
raise Exception("Meaningful message indicating the source of the error")
79+
```
80+
81+
## Running the tests
82+
83+
To run the tests, run the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)):
84+
85+
- Python 2.7: `py.test sgf_parsing_test.py`
86+
- Python 3.3+: `pytest sgf_parsing_test.py`
87+
88+
Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version):
89+
`python -m pytest sgf_parsing_test.py`
90+
91+
### Common `pytest` options
92+
93+
- `-v` : enable verbose output
94+
- `-x` : stop running tests on first failure
95+
- `--ff` : run failures from previous test before running other test cases
96+
97+
For other options, see `python -m pytest -h`
98+
99+
## Submitting Exercises
100+
101+
Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/sgf-parsing` directory.
102+
103+
You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
104+
105+
For more detailed information about running tests, code style and linting,
106+
please see the [help page](http://exercism.io/languages/python).
107+
108+
## Submitting Incomplete Solutions
109+
110+
It's possible to submit an incomplete solution so you can see how others have completed the exercise.

exercises/sgf-parsing/example.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
class SgfTree(object):
2+
def __init__(self, properties=None, children=None):
3+
self.properties = properties or {}
4+
self.children = children or []
5+
6+
def __eq__(self, other):
7+
if not isinstance(other, SgfTree):
8+
return False
9+
for k, v in self.properties.items():
10+
if k not in other.properties:
11+
return False
12+
if other.properties[k] != v:
13+
return False
14+
for k in other.properties.keys():
15+
if k not in self.properties:
16+
return False
17+
if len(self.children) != len(other.children):
18+
return False
19+
for a, b in zip(self.children, other.children):
20+
if not (a == b):
21+
return False
22+
return True
23+
24+
def __repr__(self):
25+
"""Ironically, encoding to SGF is much easier"""
26+
rep = '(;'
27+
for k, vs in self.properties.items():
28+
rep += k
29+
for v in vs:
30+
rep += '[{}]'.format(v)
31+
if self.children:
32+
if len(self.children) > 1:
33+
rep += '('
34+
for c in self.children:
35+
rep += repr(c)[1:-1]
36+
if len(self.children) > 1:
37+
rep += ')'
38+
return rep + ')'
39+
40+
41+
def is_upper(s):
42+
a, z = map(ord, 'AZ')
43+
return all(
44+
a <= o and o <= z
45+
for o in map(ord, s)
46+
)
47+
48+
49+
def parse(input_string):
50+
root = None
51+
current = None
52+
stack = list(input_string)
53+
54+
def assert_that(condition):
55+
if not condition:
56+
raise ValueError(
57+
'invalid format at {}:{}: {}'.format(
58+
repr(input_string),
59+
len(input_string) - len(stack),
60+
repr(''.join(stack))
61+
)
62+
)
63+
assert_that(stack)
64+
65+
def pop():
66+
if stack[0] == '\\':
67+
stack.pop(0)
68+
ch = stack.pop(0)
69+
return ' ' if ch in '\n\t' else ch
70+
71+
def peek():
72+
return stack[0]
73+
74+
def pop_until(ch):
75+
v = ''
76+
while peek() != ch:
77+
v += pop()
78+
return v
79+
while stack:
80+
assert_that(pop() == '(' and peek() == ';')
81+
while pop() == ';':
82+
properties = {}
83+
while is_upper(peek()):
84+
key = pop_until('[')
85+
assert_that(is_upper(key))
86+
values = []
87+
while peek() == '[':
88+
pop()
89+
values.append(pop_until(']'))
90+
pop()
91+
properties[key] = values
92+
if root is None:
93+
current = root = SgfTree(properties)
94+
else:
95+
current = SgfTree(properties)
96+
root.children.append(current)
97+
while peek() == '(':
98+
child_input = pop() + pop_until(')') + pop()
99+
current.children.append(parse(child_input))
100+
return root

exercises/sgf-parsing/sgf_parsing.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class SgfTree(object):
2+
def __init__(self, properties=None, children=None):
3+
self.properties = properties or {}
4+
self.children = children or []
5+
6+
def __eq__(self, other):
7+
if not isinstance(other, SgfTree):
8+
return False
9+
for k, v in self.properties.items():
10+
if k not in other.properties:
11+
return False
12+
if other.properties[k] != v:
13+
return False
14+
for k in other.properties.keys():
15+
if k not in self.properties:
16+
return False
17+
if len(self.children) != len(other.children):
18+
return False
19+
for a, b in zip(self.children, other.children):
20+
if a != b:
21+
return False
22+
return True
23+
24+
25+
def parse(input_string):
26+
pass
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import unittest
2+
3+
from sgf_parsing import parse, SgfTree
4+
5+
6+
class SgfParsingTest(unittest.TestCase):
7+
def test_empty_input(self):
8+
input_string = ''
9+
with self.assertRaisesWithMessage(ValueError):
10+
parse(input_string)
11+
12+
def test_tree_with_no_nodes(self):
13+
input_string = '()'
14+
with self.assertRaisesWithMessage(ValueError):
15+
parse(input_string)
16+
17+
def test_node_without_tree(self):
18+
input_string = ';'
19+
with self.assertRaisesWithMessage(ValueError):
20+
parse(input_string)
21+
22+
def test_node_without_properties(self):
23+
input_string = '(;)'
24+
expected = SgfTree()
25+
self.assertEqual(parse(input_string), expected)
26+
27+
def test_single_node_tree(self):
28+
input_string = '(;A[B])'
29+
expected = SgfTree(properties={'A': ['B']})
30+
self.assertEqual(parse(input_string), expected)
31+
32+
def test_properties_without_delimiter(self):
33+
input_string = '(;a)'
34+
with self.assertRaisesWithMessage(ValueError):
35+
parse(input_string)
36+
37+
def test_all_lowercase_property(self):
38+
input_string = '(;a[b])'
39+
with self.assertRaisesWithMessage(ValueError):
40+
parse(input_string)
41+
42+
def test_upper_and_lowercase_property(self):
43+
input_string = '(;Aa[b])'
44+
with self.assertRaisesWithMessage(ValueError):
45+
parse(input_string)
46+
47+
def test_two_nodes(self):
48+
input_string = '(;A[B];B[C])'
49+
expected = SgfTree(
50+
properties={'A': ['B']},
51+
children=[
52+
SgfTree({'B': ['C']})
53+
]
54+
)
55+
self.assertEqual(parse(input_string), expected)
56+
57+
def test_two_child_trees(self):
58+
input_string = '(;A[B](;B[C])(;C[D]))'
59+
expected = SgfTree(
60+
properties={'A': ['B']},
61+
children=[
62+
SgfTree({'B': ['C']}),
63+
SgfTree({'C': ['D']}),
64+
]
65+
)
66+
self.assertEqual(parse(input_string), expected)
67+
68+
def test_multiple_property_values(self):
69+
input_string = '(;A[b][c][d])'
70+
expected = SgfTree(
71+
properties={'A': ['b', 'c', 'd']}
72+
)
73+
self.assertEqual(parse(input_string), expected)
74+
75+
def test_escaped_property(self):
76+
input_string = '(;A[\]b\nc\nd\t\te \n\]])'
77+
expected = SgfTree(
78+
properties={'A': [']b c d e ]']}
79+
)
80+
self.assertEqual(parse(input_string), expected)
81+
82+
# Utility functions
83+
def setUp(self):
84+
try:
85+
self.assertRaisesRegex
86+
except AttributeError:
87+
self.assertRaisesRegex = self.assertRaisesRegexp
88+
89+
def assertRaisesWithMessage(self, exception):
90+
return self.assertRaisesRegex(exception, r".+")
91+
92+
93+
if __name__ == '__main__':
94+
unittest.main()

0 commit comments

Comments
 (0)