6
\$\begingroup\$

My goal is to take an input stream (text file), and then parse it into a tree node based on each line's indentation.

My rules are

  1. Lines can be indented using either tabs or spaces, but the file must only consist of one type of indentation.
  2. A line can be indicated as a child by having a larger indent than the previous line.
  3. The number of characters you use to indent is not important, 2,3,4 spaces (or tabs), anything is okay.
  4. You can specify a sibling node by giving it the same number of indent characters as any previous line.
  5. When unindenting, you can only unindent back to a previously used indentation level. So you cannot indent from 0 to 4, then indent 4 to 8, and then unindent to 5.
// The class to hold the tree
[DebuggerDisplay("Text = {Text}")]
internal class NestedTextNode
{
    public int LineNumber { get; private set; }
    public string Text { get; private set; }
    public ImmutableArray<NestedTextNode> Children { get; private set; }

    public NestedTextNode(int lineNumber, string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            throw new ArgumentException(paramName: nameof(text), message: "Required");

        LineNumber = lineNumber;
        Text = text;
        Children = ImmutableArray.Create<NestedTextNode>();
    }

    public NestedTextNode AddChild(int lineNumber, string content)
    {
        var child = new NestedTextNode(lineNumber, content);
        Children = Children.Add(child);
        return child;
    }
}
// The class to parse the contents
internal static class IndentedTextParser
{
    private static readonly Regex SplitIndentAndText = new Regex(@"^(\s*)(.+)$", RegexOptions.Compiled);

    internal static ImmutableArray<NestedTextNode> Parse(StreamReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);

        var parentNodes = new Stack<KeyValuePair<int, NestedTextNode>>();

        int lineNumber = 0;
        int firstIndentedLineNumber = 0;
        int previousIndentLevel = -1;
        var rootNode = new NestedTextNode(-1, "root");
        parentNodes.Push(new(-1, rootNode));
        NestedTextNode? currentNode = rootNode;
        string? line;
        char? indentationChar = null;
        while ((line = reader.ReadLine()) is not null)
        {
            lineNumber++;
            if (string.IsNullOrWhiteSpace(line))
                continue;

            var match = SplitIndentAndText.Match(line);
            string indent = match.Groups[1].Value;
            string content = match.Groups[2].Value;

            int indentLevel = GetIndentLevel(indent, lineNumber, ref indentationChar, ref firstIndentedLineNumber);

            if (indentLevel <= previousIndentLevel)
            {
                if (indentLevel < previousIndentLevel)
                    EnsureIsAtSiblingLevel(parentNodes, lineNumber, previousIndentLevel, indentLevel);
                // New node added to parent to make a sibling
                currentNode = parentNodes.Peek().Value.AddChild(lineNumber, content);
            }
            else if (indentLevel > previousIndentLevel)
            {
                // New node added to current node to make a child
                parentNodes.Push(new(indentLevel, currentNode));
                currentNode = currentNode.AddChild(lineNumber, content);
            }

            previousIndentLevel = indentLevel;
        }
        return rootNode.Children;
    }

    private static void EnsureIsAtSiblingLevel(
        Stack<KeyValuePair<int, NestedTextNode>> parentNodes,
        int lineNumber,
        int previousIndentLevel,
        int indentLevel)
    {
        do
        {
            parentNodes.Pop();
            if (parentNodes.Count == 1)
                throw new TestCaseException(
                    $"Line {lineNumber} unindents from indent {previousIndentLevel} to {indentLevel}" +
                    $" but no previous sibling node was found indented to {indentLevel}");
        } while (parentNodes.Peek().Key != indentLevel);
    }

    private static int GetIndentLevel(
        string indentText,
        int lineNumber,
        ref char? indentationChar,
        ref int firstIdentedLineNumber)
    {
        ArgumentNullException.ThrowIfNull(indentText);
        if (indentText.Length > 0)
        {
            if (indentationChar is null)
            {
                indentationChar = indentText[0];
                firstIdentedLineNumber = lineNumber;
            }

            string? correctIndentNamePlural = null;
            string? incorrectIndentNameSingular = null;

            if (indentationChar == ' ' && indentText.Contains('\t'))
            {
                correctIndentNamePlural = "spaces";
                incorrectIndentNameSingular = "tab";
            }
            else if (indentationChar == '\t' && indentText.Contains(' '))
            {
                correctIndentNamePlural = "tabs";
                incorrectIndentNameSingular = "space";
            }

            if (correctIndentNamePlural is not null)
                throw new TestCaseException(
                    $"Based the first character on line {firstIdentedLineNumber}" +
                    $" you should be using {correctIndentNamePlural} for indents" +
                    $" but found a {incorrectIndentNameSingular} on line {lineNumber}");
        }

        return indentText.Length;
    }
}

Example

# When all players agree on a highest bet, we should progress to post-flop

Arrange
    With new game
        Minimum bet: 32
        Next cards: 2 Clubs, 3 Clubs, 4 Clubs, 5 Clubs, 6 Clubs, 7 Clubs, 8 Clubs, 9 Clubs, 10 Clubs
        Players
            Player  | Opening balance
            1       | 1000
            2       | 2000
            3       | 3000

Act
    Player 3: Raise to 64
    Player 1: Call
    Player 2: Call

Assert
    Game has state
        Step: Post-flop betting
        Highest bet: 64
        Current player: 1
        Shared cards: 8 Clubs, 9 Clubs, 10 Clubs
        Players
            Player  | Current bet   | Status
            1       | 64            | In play
            2       | 64            | In play
            3       | 64            | In play
\$\endgroup\$
0

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.