。コメントで指摘車輪の再発明するための良い理由はほとんど、教育目的から離れて、ありません。それは書き込みパーサに来るとき、それは、ハード権利を取得するために作るそれらのコーナーケースです:
- フォーマットの検証、より具体的に割り当て、パフォーマンスのためにコードを最適化する
- ユーザーに優しい メッセージを返し、並行性、...
つまり、C#7のパターンマッチングのサポートでは、基本的な配管は実装するのに十分なはずです。
最初の停止、Node
クラス、初期化を追加。
public class Node
{
public Node(string name)
{
Name = name;
InnerNodes = new List<Node>();
}
public string Name { get; }
public string Contents { get; set; }
public List<Node> InnerNodes { get; }
}
次に、私たちの文法の別個の部分のためのラッパーを作成します。
internal abstract class Token
{
}
internal class OpenNodeToken : Token
{
public OpenNodeToken(string name) { Name = name; }
public string Name { get; }
}
internal class CloseNodeToken : Token
{
}
internal class ContentToken : Token
{
public ContentToken(string text) { Text = text; }
public string Text { get; }
}
また、入力文字列をトークンのシーケンスに変換するヘルパークラスです。
internal static class Tokenizer
{
public static IEnumerable<Token> Scan (string expression)
{
var words = new Queue<string>
(expression.Split(new[] { ' ', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries));
while (words.Any())
{
string word = words.Dequeue();
switch (word)
{
case "id":
yield return new OpenNodeToken(words.Dequeue());
words.Dequeue();
break;
case "}":
yield return new CloseNodeToken();
break;
default:
yield return new ContentToken(word);
break;
}
}
}
}
は私が
id
場合のために一度に複数の文字列をデキューするために
Queue
を使用しました。
最後に、パターンマッチングでは簡潔なコードが可能なパーサです。
public static class Parser
{
private static Node currNode;
private static Stack<Node> prevNodes;
private static IEnumerable<Token> tokens;
static Parser()
{
prevNodes = new Stack<Node>();
}
public static Node Deserialize(string input)
{
tokens = Tokenizer.Scan(input);
if (!(tokens.FirstOrDefault() is OpenNodeToken rootToken))
throw new FormatException("Missing root node");
currNode = new Node(rootToken.Name);
foreach(Token token in tokens.Skip(1))
{
switch (token)
{
case ContentToken c:
string s = string.IsNullOrEmpty(currNode.Contents) ? c.Text : " " + c.Text;
currNode.Contents += s;
break;
case OpenNodeToken n:
prevNodes.Push(currNode);
currNode = new Node(n.Name);
break;
case CloseNodeToken c:
if (prevNodes.Any())
{
Node childNode = currNode;
currNode = prevNodes.Pop();
currNode.InnerNodes.Add(childNode);
}
break;
default: throw new NotImplementedException(token.GetType().Name);
}
}
return currNode;
}
}
は、ここでは、子供を解析する前に親ノードをプッシュする
Stack
を使用しています。子の閉じ括弧が満たされると、スタックの親をポップし、その子をそのコレクションに追加します。
前述したように、まともなパーサもコーナーケースをカバーする必要があります。私はそれらを実装することの喜びを読者に残しました。パーサーをテストするために、以下のコンソールプロジェクトを設定しました。
namespace MySimpleParser
{
class Program
{
public static void Main(string[] args)
{
string s = GetInput();
try
{
Node root = Parser.Deserialize(s);
PrintBranch(root, 1);
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
Console.ReadLine();
}
internal static string GetInput()
{
return @"id i {
any data; any [con=tent]
id j {
any inner data
}
id k {
bla
id m {
any
}
thing
}
}";
}
internal static void PrintNode(Node n, int depth)
{
string indent = new string('-', 3 * depth);
Console.WriteLine($"{indent} Name: {n.Name}");
Console.WriteLine($"{indent} Contents: {n.Contents}");
Console.WriteLine($"{indent} Child Nodes: {n.InnerNodes.Count}");
}
internal static void PrintBranch(Node root, int depth)
{
PrintNode(root, depth);
foreach (Node child in root.InnerNodes) PrintBranch(child, depth + 1);
}
}
}
出力
--- Name: i
--- Contents: any data; any [con=tent]
--- Child Nodes: 2
------ Name: j
------ Contents: any inner data
------ Child Nodes: 0
------ Name: k
------ Contents: bla thing
------ Child Nodes: 1
--------- Name: m
--------- Contents: any
--------- Child Nodes: 0
再帰アルゴリズムを使用します。 – jdweng
パーサーを書く...第2の考えでは、なぜこれを使うのですか? JSONまたはYAMLを使用します。すでに書かれていて利用可能なパーサ –
文法は単に 'id NAME {CONTENT}'以上のものです。このデータを解析する方法の詳細を提供する必要があります。 – Enigmativity