Skip to content

Commit c49ec04

Browse files
committed
Add sgf-parsing practice exercise
- Add exercise stub (sgf-parsing.js) - Add test suite with 23 canonical test cases (sgf-parsing.spec.js) - Add reference solution (.meta/proof.ci.js) - Add .meta/config.json with file configuration - Add .meta/tests.toml with all 24 canonical test UUIDs - Register exercise in config.json with UUID 6a44570c-f9a0-4cd7-9217-fcf7f39b3de0
1 parent 3f8bb31 commit c49ec04

14 files changed

Lines changed: 673 additions & 0 deletions

File tree

‎config.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,14 @@
17361736
"text_formatting"
17371737
]
17381738
},
1739+
{
1740+
"slug": "sgf-parsing",
1741+
"name": "SGF Parsing",
1742+
"uuid": "6a44570c-f9a0-4cd7-9217-fcf7f39b3de0",
1743+
"practices": [],
1744+
"prerequisites": [],
1745+
"difficulty": 7
1746+
},
17391747
{
17401748
"slug": "diamond",
17411749
"name": "Diamond",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/node_modules
2+
/bin/configlet
3+
/bin/configlet.exe
4+
/package-lock.json
5+
/yarn.lock
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"authors": [],
3+
"files": {
4+
"solution": [
5+
"sgf-parsing.js"
6+
],
7+
"test": [
8+
"sgf-parsing.spec.js"
9+
],
10+
"example": [
11+
".meta/proof.ci.js"
12+
]
13+
},
14+
"blurb": "Parsing a Smart Game Format string."
15+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
export function parse(input) {
2+
if (!input.startsWith('(')) {
3+
throw new Error('tree missing');
4+
}
5+
6+
const parser = new SgfParser(input);
7+
return parser.parseTree();
8+
}
9+
10+
class SgfParser {
11+
constructor(input) {
12+
this.input = input;
13+
this.pos = 0;
14+
}
15+
16+
peek() {
17+
return this.input[this.pos];
18+
}
19+
20+
consume() {
21+
return this.input[this.pos++];
22+
}
23+
24+
parseTree() {
25+
if (this.peek() !== '(') {
26+
throw new Error('tree missing');
27+
}
28+
this.consume(); // consume '('
29+
30+
if (this.peek() === ')') {
31+
throw new Error('tree with no nodes');
32+
}
33+
34+
const root = this.parseNode();
35+
36+
this.consume(); // consume ')'
37+
38+
return root;
39+
}
40+
41+
parseNode() {
42+
if (this.peek() !== ';') {
43+
throw new Error('tree missing');
44+
}
45+
this.consume(); // consume ';'
46+
47+
const node = { properties: {}, children: [] };
48+
49+
// Parse properties of this node
50+
while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) {
51+
const { key, values } = this.parseProperty();
52+
node.properties[key] = values;
53+
}
54+
55+
// Parse subsequent nodes or child trees
56+
while (this.pos < this.input.length && this.peek() !== ')') {
57+
if (this.peek() === ';') {
58+
// Next node in sequence becomes the single child
59+
const child = this.parseNode();
60+
node.children.push(child);
61+
break; // parseNode handles its own children recursively
62+
} else if (this.peek() === '(') {
63+
// Variation: a child tree
64+
const child = this.parseTree();
65+
node.children.push(child);
66+
} else {
67+
break;
68+
}
69+
}
70+
71+
return node;
72+
}
73+
74+
parseProperty() {
75+
let key = '';
76+
77+
// Read key characters (must be uppercase letters)
78+
while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) {
79+
key += this.consume();
80+
}
81+
82+
if (!/^[A-Z]+$/.test(key)) {
83+
throw new Error('property must be in uppercase');
84+
}
85+
86+
if (this.peek() !== '[') {
87+
throw new Error('properties without delimiter');
88+
}
89+
90+
const values = [];
91+
92+
// A property can have multiple values: KEY[val1][val2]...
93+
while (this.peek() === '[') {
94+
this.consume(); // consume '['
95+
values.push(this.parseValue());
96+
this.consume(); // consume ']'
97+
}
98+
99+
return { key, values };
100+
}
101+
102+
parseValue() {
103+
let value = '';
104+
105+
while (this.pos < this.input.length && this.peek() !== ']') {
106+
const ch = this.consume();
107+
108+
if (ch === '\\') {
109+
const next = this.consume();
110+
if (next === '\n') {
111+
// escaped newline: remove entirely
112+
} else if (next === '\r') {
113+
// handle \r\n
114+
if (this.peek() === '\n') this.consume();
115+
} else if (next === '\t') {
116+
// escaped tab -> space
117+
value += ' ';
118+
} else {
119+
// any other escaped char: keep the char, drop the backslash
120+
value += next;
121+
}
122+
} else if (ch === '\t') {
123+
// unescaped tab -> space
124+
value += ' ';
125+
} else {
126+
value += ch;
127+
}
128+
}
129+
130+
return value;
131+
}
132+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[2668d5dc-109f-4f71-b9d5-8d06b1d6f1cd]
13+
description = "empty input"
14+
15+
[84ded10a-94df-4a30-9457-b50ccbdca813]
16+
description = "tree with no nodes"
17+
18+
[0a6311b2-c615-4fa7-800e-1b1cbb68833d]
19+
description = "node without tree"
20+
21+
[8c419ed8-28c4-49f6-8f2d-433e706110ef]
22+
description = "node without properties"
23+
24+
[8209645f-32da-48fe-8e8f-b9b562c26b49]
25+
description = "single node tree"
26+
27+
[6c995856-b919-4c75-8fd6-c2c3c31b37dc]
28+
description = "multiple properties"
29+
30+
[a771f518-ec96-48ca-83c7-f8d39975645f]
31+
description = "properties without delimiter"
32+
33+
[6c02a24e-6323-4ed5-9962-187d19e36bc8]
34+
description = "all lowercase property"
35+
36+
[8772d2b1-3c57-405a-93ac-0703b671adc1]
37+
description = "upper and lowercase property"
38+
39+
[a759b652-240e-42ec-a6d2-3a08d834b9e2]
40+
description = "two nodes"
41+
42+
[cc7c02bc-6097-42c4-ab88-a07cb1533d00]
43+
description = "two child trees"
44+
45+
[724eeda6-00db-41b1-8aa9-4d5238ca0130]
46+
description = "multiple property values"
47+
48+
[28092c06-275f-4b9f-a6be-95663e69d4db]
49+
description = "within property values, whitespace characters such as tab are converted to spaces"
50+
51+
[deaecb9d-b6df-4658-aa92-dcd70f4d472a]
52+
description = "within property values, newlines remain as newlines"
53+
54+
[8e4c970e-42d7-440e-bfef-5d7a296868ef]
55+
description = "escaped closing bracket within property value becomes just a closing bracket"
56+
57+
[cf371fa8-ba4a-45ec-82fb-38668edcb15f]
58+
description = "escaped backslash in property value becomes just a backslash"
59+
60+
[dc13ca67-fac0-4b65-b3fe-c584d6a2c523]
61+
description = "opening bracket within property value doesn't need to be escaped"
62+
63+
[a780b97e-8dbb-474e-8f7e-4031902190e8]
64+
description = "semicolon in property value doesn't need to be escaped"
65+
66+
[0b57a79e-8d89-49e5-82b6-2eaaa6b88ed7]
67+
description = "parentheses in property value don't need to be escaped"
68+
69+
[c72a33af-9e04-4cc5-9890-1b92262813ac]
70+
description = "escaped tab in property value is converted to space"
71+
72+
[3a1023d2-7484-4498-8d73-3666bb386e81]
73+
description = "escaped newline in property value is converted to nothing at all"
74+
75+
[25abf1a4-5205-46f1-8c72-53273b94d009]
76+
description = "escaped t and n in property value are just letters, not whitespace"
77+
78+
[08e4b8ba-bb07-4431-a3d9-b1f4cdea6dab]
79+
description = "mixing various kinds of whitespace and escaped characters in property value"
80+
reimplements = "11c36323-93fc-495d-bb23-c88ee5844b8c"
81+
82+
[11c36323-93fc-495d-bb23-c88ee5844b8c]
83+
description = "escaped property"
84+
include = false
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
audit=false
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Exercism
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
3+
plugins: [],
4+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// @ts-check
2+
3+
import config from '@exercism/eslint-config-javascript';
4+
import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
5+
6+
import globals from 'globals';
7+
8+
export default [
9+
...config,
10+
...maintainersConfig,
11+
{
12+
files: maintainersConfig[1].files,
13+
rules: {
14+
'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
15+
},
16+
},
17+
{
18+
files: ['scripts/**/*.mjs'],
19+
languageOptions: {
20+
globals: {
21+
...globals.node,
22+
},
23+
},
24+
},
25+
// <<inject-rules-here>>
26+
{
27+
ignores: [
28+
// # Protected or generated
29+
'/.appends/**/*',
30+
'/.github/**/*',
31+
'/.vscode/**/*',
32+
33+
// # Binaries
34+
'/bin/*',
35+
36+
// # Configuration
37+
'/config',
38+
'/babel.config.js',
39+
40+
// # Typings
41+
'/exercises/**/global.d.ts',
42+
'/exercises/**/env.d.ts',
43+
],
44+
},
45+
];

0 commit comments

Comments
 (0)