Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,14 @@
"text_formatting"
]
},
{
"slug": "sgf-parsing",
"name": "SGF Parsing",
"uuid": "6a44570c-f9a0-4cd7-9217-fcf7f39b3de0",
"practices": [],
"prerequisites": [],
"difficulty": 7
},
{
"slug": "diamond",
"name": "Diamond",
Expand Down
66 changes: 66 additions & 0 deletions exercises/practice/sgf-parsing/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Instructions

Your task is to parse a Smart Game Format (SGF) string and return a tree data structure representing the game.

An SGF string represents a tree of nodes. The entire tree is enclosed in parentheses `()`.
Each node in the tree is denoted by a semicolon `;`, followed by a sequence of properties.
A property consists of an uppercase identifier (e.g., `AB`), followed by one or more values enclosed in brackets `[]`.

For example, the string `(;FF[4]C[root])` represents a tree with a single root node. That node has two properties:
- Property `FF` with a value of `4`
- Property `C` with a value of `root`

**Variations (Trees and Branches)**
SGF supports game variations by nesting trees. A tree can have multiple child trees, which are also enclosed in parentheses.
For example, `(;A[B](;C[D])(;E[F]))` represents a root node with property `A`, which has two child branches:
1. A node with property `C` and value `D`
2. A node with property `E` and value `F`

**Formatting & Escaping**
Values within brackets can contain escaped characters. A backslash `\` followed by any character represents that literal character (e.g., `\]` should be parsed as a literal `]`). Additionally, if a newline is preceded by a backslash, it acts as a line continuation and should be removed from the value. All other newlines within a value should be preserved.

**Error Handling**
You must throw an error for invalid SGF strings. The following are examples of invalid SGF:
- A tree that does not start with parentheses `()`.
- A node that does not start with a semicolon `;`.
- Property identifiers that are not entirely uppercase letters.
- Missing brackets or mismatched parentheses.

### Data Structure

Your parsing function should return a representation of the tree. A common approach is to represent each tree as an object with two fields:
- `properties`: An object (or Map) where the keys are the property identifiers (strings) and the values are arrays of strings (since a property can have multiple values).
- `children`: An array containing the child trees.


Example:

Input: `(;FF[4]C[root])`
Output: `{
properties: {
FF: ['4'],
C: ['root']
},
children: []
}`

Input: `(;A[B](;C[D])(;E[F]))`
Output: `{
properties: {
A: ['B']
},
children: [
{
properties: {
C: ['D']
},
children: []
},
{
properties: {
E: ['F']
},
children: []
}
]
}`
7 changes: 7 additions & 0 deletions exercises/practice/sgf-parsing/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Introduction

SGF (Smart Game Format) is a popular, text-based file format used for storing records of board games. While it can be used for many different games like Chess, Othello, and Backgammon, it is most widely known and used for the game of Go.

An SGF file is structured as a tree of nodes. This tree structure is particularly useful because it allows game records to branch out, meaning you can store the main sequence of moves as well as alternative variations, analyses, and commentary all in the same file.

Each node in the tree contains a set of properties, and each property has one or more values. These properties encode everything from the players' names and ranks, to the game result, board size, and individual moves placed on the board.
5 changes: 5 additions & 0 deletions exercises/practice/sgf-parsing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/node_modules
/bin/configlet
/bin/configlet.exe
/package-lock.json
/yarn.lock
24 changes: 24 additions & 0 deletions exercises/practice/sgf-parsing/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"authors": [
"matthewmorgan"
],
"files": {
"solution": [
"sgf-parsing.js"
],
"test": [
"sgf-parsing.spec.js"
],
"example": [
".meta/proof.ci.js"
]
},
"blurb": "Parsing a Smart Game Format string.",
"source_url": "https://en.wikipedia.org/wiki/Smart_Game_Format",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
"flag.tests.may-run-long": false,
"flag.tests.includes-optional": false
}
}
132 changes: 132 additions & 0 deletions exercises/practice/sgf-parsing/.meta/proof.ci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
export function parse(input) {
if (!input.startsWith('(')) {
throw new Error('tree missing');
}

const parser = new SgfParser(input);
return parser.parseTree();
}

class SgfParser {
constructor(input) {
this.input = input;
this.pos = 0;
}

peek() {
return this.input[this.pos];
}

consume() {
return this.input[this.pos++];
}

parseTree() {
if (this.peek() !== '(') {
throw new Error('tree missing');
}
this.consume(); // consume '('

if (this.peek() === ')') {
throw new Error('tree with no nodes');
}

const root = this.parseNode();

this.consume(); // consume ')'

return root;
}

parseNode() {
if (this.peek() !== ';') {
throw new Error('tree missing');
}
this.consume(); // consume ';'

const node = { properties: {}, children: [] };

// Parse properties of this node
while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) {
const { key, values } = this.parseProperty();
node.properties[key] = values;
}

// Parse subsequent nodes or child trees
while (this.pos < this.input.length && this.peek() !== ')') {
if (this.peek() === ';') {
// Next node in sequence becomes the single child
const child = this.parseNode();
node.children.push(child);
break; // parseNode handles its own children recursively
} else if (this.peek() === '(') {
// Variation: a child tree
const child = this.parseTree();
node.children.push(child);
} else {
break;
}
}

return node;
}

parseProperty() {
let key = '';

// Read key characters (must be uppercase letters)
while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) {
key += this.consume();
}

if (!/^[A-Z]+$/.test(key)) {
throw new Error('property must be in uppercase');
}

if (this.peek() !== '[') {
throw new Error('properties without delimiter');
}

const values = [];

// A property can have multiple values: KEY[val1][val2]...
while (this.peek() === '[') {
this.consume(); // consume '['
values.push(this.parseValue());
this.consume(); // consume ']'
}

return { key, values };
}

parseValue() {
let value = '';

while (this.pos < this.input.length && this.peek() !== ']') {
const ch = this.consume();

if (ch === '\\') {
const next = this.consume();
if (next === '\n') {
// escaped newline: remove entirely
} else if (next === '\r') {
// handle \r\n
if (this.peek() === '\n') this.consume();
} else if (next === '\t') {
// escaped tab -> space
value += ' ';
} else {
// any other escaped char: keep the char, drop the backslash
value += next;
}
} else if (ch === '\t') {
// unescaped tab -> space
value += ' ';
} else {
value += ch;
}
}

return value;
}
}
84 changes: 84 additions & 0 deletions exercises/practice/sgf-parsing/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[2668d5dc-109f-4f71-b9d5-8d06b1d6f1cd]
description = "empty input"

[84ded10a-94df-4a30-9457-b50ccbdca813]
description = "tree with no nodes"

[0a6311b2-c615-4fa7-800e-1b1cbb68833d]
description = "node without tree"

[8c419ed8-28c4-49f6-8f2d-433e706110ef]
description = "node without properties"

[8209645f-32da-48fe-8e8f-b9b562c26b49]
description = "single node tree"

[6c995856-b919-4c75-8fd6-c2c3c31b37dc]
description = "multiple properties"

[a771f518-ec96-48ca-83c7-f8d39975645f]
description = "properties without delimiter"

[6c02a24e-6323-4ed5-9962-187d19e36bc8]
description = "all lowercase property"

[8772d2b1-3c57-405a-93ac-0703b671adc1]
description = "upper and lowercase property"

[a759b652-240e-42ec-a6d2-3a08d834b9e2]
description = "two nodes"

[cc7c02bc-6097-42c4-ab88-a07cb1533d00]
description = "two child trees"

[724eeda6-00db-41b1-8aa9-4d5238ca0130]
description = "multiple property values"

[28092c06-275f-4b9f-a6be-95663e69d4db]
description = "within property values, whitespace characters such as tab are converted to spaces"

[deaecb9d-b6df-4658-aa92-dcd70f4d472a]
description = "within property values, newlines remain as newlines"

[8e4c970e-42d7-440e-bfef-5d7a296868ef]
description = "escaped closing bracket within property value becomes just a closing bracket"

[cf371fa8-ba4a-45ec-82fb-38668edcb15f]
description = "escaped backslash in property value becomes just a backslash"

[dc13ca67-fac0-4b65-b3fe-c584d6a2c523]
description = "opening bracket within property value doesn't need to be escaped"

[a780b97e-8dbb-474e-8f7e-4031902190e8]
description = "semicolon in property value doesn't need to be escaped"

[0b57a79e-8d89-49e5-82b6-2eaaa6b88ed7]
description = "parentheses in property value don't need to be escaped"

[c72a33af-9e04-4cc5-9890-1b92262813ac]
description = "escaped tab in property value is converted to space"

[3a1023d2-7484-4498-8d73-3666bb386e81]
description = "escaped newline in property value is converted to nothing at all"

[25abf1a4-5205-46f1-8c72-53273b94d009]
description = "escaped t and n in property value are just letters, not whitespace"

[08e4b8ba-bb07-4431-a3d9-b1f4cdea6dab]
description = "mixing various kinds of whitespace and escaped characters in property value"
reimplements = "11c36323-93fc-495d-bb23-c88ee5844b8c"

[11c36323-93fc-495d-bb23-c88ee5844b8c]
description = "escaped property"
include = false
1 change: 1 addition & 0 deletions exercises/practice/sgf-parsing/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
audit=false
21 changes: 21 additions & 0 deletions exercises/practice/sgf-parsing/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Exercism

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions exercises/practice/sgf-parsing/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
Loading
Loading