LoginSignup
37
36

More than 5 years have passed since last update.

C#でJSONパーサ

Posted at

今日はJSONの構文解析器を作ってみたいと思います。

構文解析器は文字列から抽象構文木を生成するのによく使われます(実は今回は抽象構文木を生成する訳ではないのですが)。

文字列 → 抽象構文木

この、文字列から抽象構文木を生成する方法には主に2種類あります。

1つは、字句解析器によって字句解析を行い、その後、構文解析器によって構文解析を行い、抽象構文木を生成する方法です。この場合は、字句解析器によって文字列を一旦字句列に変換し、その後、構文解析器によって字句列を抽象構文木に変換することになります。

文字列 → <字句解析器> → 字句列 → <構文解析器> → 抽象構文木

もう1つは、構文解析器によって直接文字列の構文解析を行い、抽象構文木を生成する方法です。この場合は、構文解析の過程で字句解析に相当する処理も纏めて行うことになります。

文字列 → <構文解析器> → 抽象構文木

また、構文解析器は抽象構文木を生成するものという訳ではなく、今回のように、JSON形式の文字列からJSON形式のオブジェクト(プログラムの中でJSON形式のデータを何らかの形で構造化したもの)を生成するためにも使えます(他にも様々な用途に使用できます)。要は、結果として生成される何かの種類を問わず、内部で構文解析を行うものを構文解析器と言うと考えれば良いでしょう。

今回は、JSON形式の文字列からJSON形式のオブジェクトを生成するための構文解析器を作成します。また、字句解析器は使わず、JSON形式の文字列を直接JSON形式のオブジェクトに変換することにします。

JSON形式の文字列 → <今日作る構文解析器> → JSON形式のオブジェクト

文字列クラスのインターフェイス

さて、今日作る構文解析器の入力は(JSON形式の)文字列であるということは先程説明した通りですが、文字列を単純にString型として構文解析器に入力するのは少々不便です。

構文解析器は入力の文字列を冒頭から1文字ずつ読み進めながら構文解析していきますので(場合によっては前の文字を読み返すこともあります)、文字列をラップして、文字列を1文字ずつ読み進められるようにしたクラスの実体を入力として受け取った方が都合が良いです。

また、構文解析において何らかのエラーが発生した場合には、文字列の何行目の何文字目でエラーが発生したかが分かった方が都合が良いです。

そこで、構文解析器の入力としては単なるString型の文字列ではなく、次のようなITextインターフェイスを提供する実体を受け取ることにしましょう。

    //文字列中の位置に関する情報
    public class PositionInfo
    {
        public PositionInfo(string _line, int _lineNumber, int _linePosition)
        {
            Line = _line;
            LineNumber = _lineNumber;
            LinePosition = _linePosition;
        }

        //文字列中の位置における行の文字列
        public string Line { get; private set; }
        //文字列中の位置が何行目であるか
        public int LineNumber { get; private set; }
        //文字列中の位置が(その行の)何文字目であるか
        public int LinePosition { get; private set; }
    }

    //文字列を1文字ずつ処理するためのインターフェイス
    public interface IText
    {
        //文字列中の現在位置に関する情報
        PositionInfo PositionInfo { get; }

        //現在位置の文字を返す
        char Peek();
        //現在位置を1文字分進める
        void Advance();
    }

ITextインターフェイスを実装するクラスは文字列中の現在位置に関する情報を返すプロパティや現在位置の文字を返すメソッドや現在位置を1文字分進めるメソッドを実装しなければなりません。

ただ、これだけでは不便なので次のような拡張関数を定義します。

    //文字列を1文字ずつ処理するためのインターフェイスに対する拡張
    public static class ITextExtension
    {
        //現在位置を1文字分進め、現在位置の文字を返す
        public static char AdvancePeek(this IText itext)
        {
            itext.Advance();

            return itext.Peek();
        }
    }

文字列クラス

前項のインターフェイスを実装する文字列クラスを作成します。文字列の末尾は\0とします。

    //現在位置が文字列の範囲外である場合に投げられる例外
    public class OutOfRangeException : Exception { }

    //文字列クラス
    //文字列を1文字ずつ処理するためのインターフェイスを実装する
    public class Text : IText
    {
        //文字列を受け取る
        public Text(string s)
        {
            //文字列の末尾を示す文字
            const string nullchar = "\0";

            //文字列の末尾が文字列の末尾を示す文字でない場合には文字列に末尾を示す文字を追加する
            //そうでない場合には何もしない
            if (!s.EndsWith(nullchar))
                s += nullchar;

            //文字列を行毎に分割する(ただし、それぞれの行の末尾には改行文字を追加する)
            lines = s.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Select((elem) => elem + Environment.NewLine).ToArray();
            //文字列の現在の行を0行目とする
            lineNumber = 0;
            //文字列の現在の行の現在の文字の位置を0文字目とする
            linePosition = 0;
        }

        //文字列の全ての行
        private string[] lines;
        //文字列の現在の行
        private int lineNumber;
        //文字列の現在の行の現在の文字の位置
        private int linePosition;

        //文字列中の現在位置に関する情報
        public PositionInfo PositionInfo
        {
            get
            {
                try
                {
                    return new PositionInfo(lines[lineNumber], lineNumber, linePosition);
                }
                catch (IndexOutOfRangeException)
                {
                    //現在位置が文字列の範囲外である場合には例外を投げる
                    throw new OutOfRangeException();
                }
            }
        }

        //現在位置の文字を返す
        public char Peek()
        {
            try
            {
                return lines[lineNumber][linePosition];
            }
            catch (IndexOutOfRangeException)
            {
                //現在位置が文字列の範囲外である場合には例外を投げる
                throw new OutOfRangeException();
            }
        }

        //現在位置を1文字分進める
        public void Advance()
        {
            try
            {
                //現在の文字が現在の行の末尾に位置する場合には現在の行を次の行に更新し、現在の文字の位置を0文字目とする
                if (linePosition == lines[lineNumber].Length - 1)
                {
                    lineNumber++;
                    linePosition = 0;
                }
                //そうでない場合には現在の文字の位置を1進める
                else
                    linePosition++;
            }
            catch (IndexOutOfRangeException)
            {
                //現在位置が文字列の範囲外である場合には例外を投げる
                throw new OutOfRangeException();
            }
        }
    }

構文解析

構文解析器は入力の文字列を冒頭から1文字ずつ読み進めながら構文解析していきますが、更に言えば読み取った文字が現在の文脈に照らして適切であるかどうかを判断していくことによって構文解析を行います。文字を読み進めるにつれて文脈も絶えず変化していくことになりますが、読み取った文字がその文脈に沿った文字でなければならないということです。不適切な文字であった場合にはエラーということになります。

すなわち、構文解析の処理は

  • 現在位置の文字の読み取り
  • 現在位置の移動
  • 現在位置の文字が妥当であるか(文脈に即しているか)の検証
  • 文脈の変更
  • 出力の作成

などの組み合わせであるということです。

これらの処理はある程度汎用的な処理(JSONの構文解析器だけではなくどのような構文解析器でも共通して使える処理)として実装できそうです。

そこで、これらの処理を汎用的な処理として一纏めにしたクラスを別途作成することにしましょう(実は、ただ単に汎用的な処理を提供するという方法以外に、単純な汎用的なパーサを幾つか提供し、それらの単純なパーサを組み合わせて複雑なパーサを作るという方法もあります。こちらの方がより洗練された方法であると言えますが、それに関しては別稿で取り上げることがあるかもしれません。今回は敢えて泥臭い方法を使っています)。

    //構文解析の汎用的な処理を行う
    public class Scanner
    {
        //文字列クラスを受け取る
        public Scanner(IText _itext)
        {
            //文字列クラスを設定する
            Itext = _itext;
            //現在の文字列を空にする
            Scan = string.Empty;
            //現在位置の文字を設定する
            Current = Itext.Peek();
        }

        //文字列クラス
        public IText Itext { get; private set; }
        //現在の文字列
        public string Scan { get; private set; }
        //現在位置の文字
        public char Current { get; private set; }

        //現在の文字列を空にする
        public void Empty()
        {
            Scan = string.Empty;
        }

        //現在位置の文字が述語を充足するか確認する
        public bool Check(Func<char, bool> func)
        {
            return func(Current);
        }

        //現在の文字列に文字列を付加する
        public void Add(string s)
        {
            Scan += s;
        }

        //現在位置を1文字分進め、現在位置の文字を更新する
        public void Advance()
        {
            Current = Itext.AdvancePeek();
        }

        //現在の文字列に現在位置の文字を付加し、現在位置を1文字分進め、現在位置の文字を更新する
        public void AddAdvance()
        {
            Scan += Current;
            Current = Itext.AdvancePeek();
        }

        //現在位置の文字が述語を充足するか確認し、充足する場合には現在位置を1文字分進め、現在位置の文字を更新する
        public bool CheckAdvance(Func<char, bool> func)
        {
            if (func(Current))
            {
                Current = Itext.AdvancePeek();

                return true;
            }

            return false;
        }

        //現在位置以降で与えられた文字列と一致する位置まで現在位置を進め、現在位置の文字を更新する
        public bool CheckAdvance(string s)
        {
            //与えられた文字列の現在の対象位置
            int i = 0;

            //与えられた文字列の現在の対象位置の文字と一致するかを確認する述語
            Func<char, bool> func = (_) => _ == s[i];

            //与えられた文字列の冒頭から末尾まで述語を充足するか確認する
            //充足しなくなった時点で処理を終了する
            for (; i < s.Length; i++)
                if (!CheckAdvance(func))
                    return false;

            return true;
        }

        //現在位置の文字が述語を充足するか確認し、充足する場合には現在位置を1文字分進め、現在位置の文字を更新する
        //ただし、充足しなくなるまで反復する
        public bool CheckAdvanceLoop(Func<char, bool> func)
        {
            bool b = false;

            while (CheckAdvance(func))
                b = true;

            return b;
        }

        //現在位置の文字が述語を充足するか確認し、充足する場合には現在の文字列に現在位置の文字を付加し、現在位置を1文字分進め、現在位置の文字を更新する
        public bool CheckAddAdvance(Func<char, bool> func)
        {
            if (func(Current))
            {
                Scan += Current;

                Current = Itext.AdvancePeek();

                return true;
            }

            return false;
        }

        //現在位置の文字が述語を充足するか確認し、充足する場合には現在の文字列に現在位置の文字を付加し、現在位置を1文字分進め、現在位置の文字を更新する
        //ただし、充足しなくなるまで反復する
        public bool CheckAddAdvanceLoop(Func<char, bool> func)
        {
            bool b = false;

            while (CheckAddAdvance(func))
                b = true;

            return b;
        }
    }

構文解析(JSON)

前項で作成したクラスを使ってJSONの構文解析器を作成しましょう。

まず、構文解析が失敗した場合に投げる例外を作成します。この例外は構文解析が失敗した位置に関する情報を含みます。

    //構文解析が失敗した場合に投げられる例外
    public class ParserException : Exception
    {
        public ParserException(string _message, PositionInfo _positionInfo) : base(_message)
        {
            PositionInfo = _positionInfo;
        }

        //文字列中で構文解析が失敗した位置に関する情報
        public PositionInfo PositionInfo { get; private set; }
    }


更に便利な拡張関数を幾つか追加しておきましょう。

    //文字に対する拡張
    public static class CharExtensions
    {
        //制御文字か
        public static bool IsControl(this char c)
        {
            return c >= 0 && c <= 0x1F;
        }

        //空白文字か
        public static bool IsWhitespace(this char c)
        {
            return c == ' ' || c == '\t' || c == '\n' || c == '\r';
        }

        //数字か
        public static bool IsDigit(this char c)
        {
            return c >= '0' && c <= '9';
        }

        //16進数の数字か
        public static bool IsHexDigit(this char c)
        {
            return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
        }

        //文字を16進数の数字として解釈する
        public static int ToHexValue(this char c)
        {
            //16進数の数字でない場合には例外を投げる
            if (!c.IsHexDigit())
                throw new Exception();

            return c >= '0' && c <= '9' ? c - '0' : char.ToUpper(c) - 'A' + 10;
        }
    }

実際の構文解析の処理はクラスの中に格納することにします。

    //JSONの構文解析を行う
    public static class JsonParser
    {
    }

構文解析に必要な、文字の種類を確認する関数を定義します。

        //文字が符号であるか
        private static Func<char, bool> IsSign = (c) => c == '-' || c == '+';
        //文字が数字であるか
        private static Func<char, bool> IsDigit = (c) => c.IsDigit();
        //文字が小数点であるか
        private static Func<char, bool> IsDecimalPoint = (c) => c == '.';
        //文字がeかEであるか
        private static Func<char, bool> IsExponent = (c) => c == 'e' || c == 'E';
        //文字が二重引用符であるか
        private static Func<char, bool> IsDoubleQuote = (c) => c == '"';
        //文字が逆斜線であるか
        private static Func<char, bool> IsBackslash = (c) => c == '\\';
        //文字が斜線であるか
        private static Func<char, bool> IsSlash = (c) => c == '/';
        //文字がbであるか
        private static Func<char, bool> IsSmallB = (c) => c == 'b';
        //文字がfであるか
        private static Func<char, bool> IsSmallF = (c) => c == 'f';
        //文字がnであるか
        private static Func<char, bool> IsSmallN = (c) => c == 'n';
        //文字がrであるか
        private static Func<char, bool> IsSmallR = (c) => c == 'r';
        //文字がtであるか
        private static Func<char, bool> IsSmallT = (c) => c == 't';
        //文字がuであるか
        private static Func<char, bool> IsSmallU = (c) => c == 'u';
        //文字が制御文字であるか
        private static Func<char, bool> IsControl = (c) => c.IsControl();
        //文字が空白文字であるか
        private static Func<char, bool> IsWhitespace = (c) => c.IsWhitespace();
        //文字が左角括弧であるか
        private static Func<char, bool> IsLeftBracket = (c) => c == '[';
        //文字が右角括弧であるか
        private static Func<char, bool> IsRightBracket = (c) => c == ']';
        //文字がカンマであるか
        private static Func<char, bool> IsComma = (c) => c == ',';
        //文字が左波括弧であるか
        private static Func<char, bool> IsLeftBrace = (c) => c == '{';
        //文字が右波括弧であるか
        private static Func<char, bool> IsRightBrace = (c) => c == '}';
        //文字がコロンであるか
        private static Func<char, bool> IsColon = (c) => c == ':';

数値を構文解析する関数を追加します。数値は0個又は1個の符号から始まり、0個以上の数字が続き、0個又は1個の小数点が続き、0個以上の数字が続き、0個又は1個のe又はEが続き、0個又は1個のe又はEが続く場合にのみ0個又は1個の符号が続き、0個以上の数字が続く文字列です。全ての数値はdouble型の値として解釈することにします(大抵の数値型の値はdouble型の値の範囲に含まれるため)。

        //数値を構文解析する
        private static double ParseNumber(Scanner scanner)
        {
            //現在の文字列を空にする
            scanner.Empty();

            //現在位置の文字が符号であるか確認する
            //符号である場合には現在の文字列に付加する
            //更に現在位置を1文字分進める
            scanner.CheckAddAdvance(IsSign);

            //現在位置の文字が数字であるか確認する
            //数字である場合には現在の文字列に付加する
            //更に現在位置を1文字分進める
            //これを現在位置の文字が数字でなくなるまで反復する
            scanner.CheckAddAdvanceLoop(IsDigit);

            //現在位置の文字が小数点であるか確認する
            //小数点である場合には現在の文字列に付加する
            //更に現在位置を1文字分進める
            scanner.CheckAddAdvance(IsDecimalPoint);

            //現在位置の文字が数字であるか確認する
            //数字である場合には現在の文字列に付加する
            //更に現在位置を1文字分進める
            //これを現在位置の文字が数字でなくなるまで反復する
            scanner.CheckAddAdvanceLoop(IsDigit);

            //現在位置の文字がeかEであるか確認する
            //eかEである場合には現在の文字列に付加する
            //更に現在位置を1文字分進める
            if (scanner.CheckAddAdvance(IsExponent))
            {
                //現在位置の文字が符号であるか確認する
                //符号である場合には現在の文字列に付加する
                //更に現在位置を1文字分進める
                scanner.CheckAddAdvance(IsSign);

                //現在位置の文字が数字であるか確認する
                //数字である場合には現在の文字列に付加する
                //更に現在位置を1文字分進める
                //これを現在位置の文字が数字でなくなるまで反復する
                scanner.CheckAddAdvanceLoop(IsDigit);
            }

            try
            {
                //現在の文字列をdouble型に変換し、返す
                return double.Parse(scanner.Scan);
            }
            //現在の文字列が数値として解釈できなかった場合には例外を投げる
            catch (FormatException)
            {
                throw new ParserException("bad format number", scanner.Itext.PositionInfo);
            }
            //現在の文字列を数値として解釈した場合の値がdouble型の値の範囲を超えている場合には例外を投げる
            catch (OverflowException)
            {
                throw new ParserException("overflow number", scanner.Itext.PositionInfo);
            }
            catch (Exception)
            {
                throw new ParserException("unexpected error number", scanner.Itext.PositionInfo);
            }
        }

文字列を構文解析する関数を追加します。基本的に文字列は二重引用符から始まり、二重引用符で終わる文字列ですが、エスケープシーケンスの処理も行わなければなりません。また、文字列には制御文字を含めることができないものとします(ただし、エスケープシーケンスを使うことによっては含めることができるものとします)。

        //文字列を構文解析する
        private static string ParseString(Scanner scanner)
        {
            //1文字後退
            const string b = "\b";
            //改ページ
            const string f = "\f";
            //改行
            const string n = "\n";
            //復帰
            const string r = "\r";
            //水平タブ
            const string t = "\t";

            //現在の文字列を空にする
            scanner.Empty();

            //現在位置の文字が二重引用符であるか確認する
            //二重引用符である場合には現在位置を1文字分進める
            //そうでない場合には例外を投げる
            if (!scanner.CheckAdvance(IsDoubleQuote))
                throw new ParserException("string -> double quotation mark required", scanner.Itext.PositionInfo);

            //現在位置の文字が二重引用符であるか確認する
            //二重引用符である場合には現在位置を1文字分進め、次の処理に進む
            while (!scanner.CheckAdvance(IsDoubleQuote))
            {
                //現在位置の文字が逆斜線であるか確認する
                //逆斜線である場合には現在位置を1文字分進める
                if (scanner.CheckAdvance(IsBackslash))
                {
                    //現在位置の文字が二重引用符であるか確認する
                    //二重引用符である場合には現在の文字列に付加する
                    //更に現在位置を1文字分進める
                    if (scanner.CheckAddAdvance(IsDoubleQuote))
                        continue;

                    //現在位置の文字が逆斜線であるか確認する
                    //逆斜線である場合には現在の文字列に付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAddAdvance(IsBackslash))
                        continue;

                    //現在位置の文字が斜線であるか確認する
                    //斜線である場合には現在の文字列に付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAddAdvance(IsSlash))
                        continue;

                    //現在位置の文字がbであるか確認する
                    //bである場合には現在の文字列に1文字後退を付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallB))
                        scanner.Add(b);

                    //現在位置の文字がfであるか確認する
                    //fである場合には現在の文字列に改ページを付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallF))
                        scanner.Add(f);

                    //現在位置の文字がnであるか確認する
                    //nである場合には現在の文字列に改行を付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallN))
                        scanner.Add(n);

                    //現在位置の文字がrであるか確認する
                    //rである場合には現在の文字列に復帰を付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallR))
                        scanner.Add(r);

                    //現在位置の文字がtであるか確認する
                    //tである場合には現在の文字列に水平タブを付加する
                    //更に現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallT))
                        scanner.Add(t);

                    //現在位置の文字がuであるか確認する
                    //uである場合には現在位置を1文字分進める
                    else if (scanner.CheckAdvance(IsSmallU))
                    {
                        //符号位置
                        int codepoint = 0;

                        //符号位置を計算する
                        //符号位置は現在位置から最大で4文字分の文字列を16進数の数値として解釈したものである
                        for (int i = 0; i < 4; i++)
                            try
                            {
                                codepoint = codepoint * 16 + scanner.Current.ToHexValue();

                                scanner.Advance();
                            }
                            catch (Exception)
                            {
                                break;
                            }

                        //現在の文字列に符号位置に対応する文字を付加する
                        scanner.Add(Convert.ToChar(codepoint).ToString());
                    }

                    //現在位置の文字が何れでもない場合には例外を投げる
                    else
                        throw new ParserException("string -> not supported escape", scanner.Itext.PositionInfo);
                }

                //現在位置の文字が制御文字である場合には例外を投げる
                else if (scanner.Check(IsControl))
                    throw new ParserException("string -> control character", scanner.Itext.PositionInfo);

                //現在位置の文字が何れでもない場合には現在の文字列に付加する
                //更に現在位置を1文字分進める
                else
                    scanner.AddAdvance();
            }

            //現在の文字列を返す
            return scanner.Scan;
        }

空白を構文解析する関数を追加します。

        //空白を構文解析する
        private static void ParseWhitespace(Scanner scanner)
        {
            //現在位置の文字が空白文字であるか確認する
            //空白文字である場合には現在位置を1文字分進める
            //これを現在位置の文字が空白文字でなくなるまで反復する
            scanner.CheckAdvanceLoop(IsWhitespace);
        }

配列を構文解析する関数を追加します。配列は左角括弧から始まり、右角括弧で終わります、その間に任意の数の値が入り、それらの間はカンマで区切られます。JSONの配列には型というものがありません。1つの配列には数値や文字列など、複数の種類の値を格納することができます。そのため、C#でJSONの配列を取り扱う場合にはobject[]型のものとして取り扱うしかないでしょう。

        //配列を構文解析する
        private static object[] ParseArray(Scanner scanner)
        {
            //配列の要素を格納するリスト
            List<object> osList = new List<object>();

            //現在位置の文字が左角括弧であるか確認する
            //左角括弧である場合には現在位置を1文字分進める
            //そうでない場合には例外を投げる
            if (!scanner.CheckAdvance(IsLeftBracket))
                throw new ParserException("array -> left bracket required", scanner.Itext.PositionInfo);

            //空白を構文解析する
            ParseWhitespace(scanner);

            //現在位置の文字が右角括弧であるか確認する
            //右角括弧である場合には現在位置を1文字分進める
            //更に配列の要素を格納するリストを配列に変換し、返す
            if (scanner.CheckAdvance(IsRightBracket))
                return osList.ToArray();

            while (true)
            {
                //値を構文解析し、配列の要素を格納するリストに追加する
                osList.Add(ParseValue(scanner));

                //空白を構文解析する
                ParseWhitespace(scanner);

                //現在位置の文字が右角括弧であるか確認する
                //右角括弧である場合には現在位置を1文字分進める
                //更に配列の要素を格納するリストを配列に変換し、返す
                if (scanner.CheckAdvance(IsRightBracket))
                    return osList.ToArray();

                //現在位置の文字がカンマであるか確認する
                //カンマである場合には現在位置を1文字分進める
                //そうでない場合には例外を投げる
                else if (!scanner.CheckAdvance(IsComma))
                    throw new ParserException("array -> comma required", scanner.Itext.PositionInfo);

                //空白を構文解析する
                ParseWhitespace(scanner);
            }
        }

オブジェクトを構文解析する関数を追加します。オブジェクトは左波括弧から始まり、右波括弧で終わります、その間に任意の数の文字列と値の組が入り、それらの間はカンマで区切られます。文字列と値の組の間はコロンで区切られます。C#ではJSONのオブジェクトはDictionary型の辞書として取り扱うことができます。オブジェクトに同一の鍵が複数含まれている場合はエラーとすることにしました。

        //オブジェクトを構文解析する
        private static Dictionary<string, object> ParseObject(Scanner scanner)
        {
            //オブジェクトの内容を格納する辞書
            Dictionary<string, object> osDict = new Dictionary<string, object>();

            //現在位置の文字が左波括弧であるか確認する
            //左波括弧である場合には現在位置を1文字分進める
            //そうでない場合には例外を投げる
            if (!scanner.CheckAdvance(IsLeftBrace))
                throw new ParserException("object -> left brace required", scanner.Itext.PositionInfo);

            //空白を構文解析する
            ParseWhitespace(scanner);

            //現在位置の文字が右波括弧であるか確認する
            //右波括弧である場合には現在位置を1文字分進める
            //更にオブジェクトの内容を格納する辞書を返す
            if (scanner.CheckAdvance(IsRightBrace))
                return osDict;

            while (true)
            {
                //文字列を構文解析し、オブジェクトの鍵とする
                string key = ParseString(scanner);

                //既に同一の鍵が存在する場合には例外を投げる
                if (osDict.Keys.Contains(key))
                    throw new ParserException("object -> same key", scanner.Itext.PositionInfo);

                //空白を構文解析する
                ParseWhitespace(scanner);

                //現在位置の文字がコロンであるか確認する
                //コロンである場合には現在位置を1文字分進める
                //そうでない場合には例外を投げる
                if (!scanner.CheckAdvance(IsColon))
                    throw new ParserException("object -> colon required", scanner.Itext.PositionInfo);

                //空白を構文解析する
                ParseWhitespace(scanner);

                //値を構文解析し、オブジェクトの鍵に対応する値とする
                osDict[key] = ParseValue(scanner);

                //空白を構文解析する
                ParseWhitespace(scanner);

                //現在位置の文字が右波括弧であるか確認する
                //右波括弧である場合には現在位置を1文字分進める
                //更にオブジェクトの内容を格納する辞書を返す
                if (scanner.CheckAdvance(IsRightBrace))
                    return osDict;

                //現在位置の文字がカンマであるか確認する
                //カンマである場合には現在位置を1文字分進める
                //そうでない場合には例外を投げる
                else if (!scanner.CheckAdvance(IsComma))
                    throw new ParserException("object -> comma required", scanner.Itext.PositionInfo);

                //空白を構文解析する
                ParseWhitespace(scanner);
            }
        }

予約語を構文解析する関数を追加します。予約語はtruefalsenullの3つです。

        //予約語を構文解析する
        private static object ParseWord(Scanner scanner)
        {
            //現在位置の文字がtであるか確認する
            if (scanner.Check(IsSmallT))
            {
                //現在位置以降がtrueで始まるか確認する
                //始まる場合には現在位置を4文字分進める
                //そうでない場合には例外を投げる
                if (!scanner.CheckAdvance("true"))
                    throw new ParserException("word -> true required", scanner.Itext.PositionInfo);

                return true;
            }
            //現在位置の文字がfであるか確認する
            else if (scanner.Check(IsSmallF))
            {
                //現在位置以降がfalseで始まるか確認する
                //始まる場合には現在位置を5文字分進める
                //そうでない場合には例外を投げる
                if (!scanner.CheckAdvance("false"))
                    throw new ParserException("word -> false required", scanner.Itext.PositionInfo);

                return false;
            }
            //現在位置の文字がnであるか確認する
            else if (scanner.Check(IsSmallN))
            {
                //現在位置以降がnullで始まるか確認する
                //始まる場合には現在位置を4文字分進める
                //そうでない場合には例外を投げる
                if (!scanner.CheckAdvance("null"))
                    throw new ParserException("word -> null required", scanner.Itext.PositionInfo);

                return null;
            }
            //現在位置の文字が何れでもない場合には例外を投げる
            else
                throw new ParserException("word -> unexpected character", scanner.Itext.PositionInfo);
        }

値を構文解析する関数を追加します。現在位置の文字に応じて構文解析を行う関数を呼び出します。

        //値を構文解析する
        private static object ParseValue(Scanner scanner)
        {
            //空白を構文解析する
            ParseWhitespace(scanner);

            //現在位置の文字が左波括弧であるか確認する
            if (scanner.Check(IsLeftBrace))
                //オブジェクトを構文解析する
                return ParseObject(scanner);
            //現在位置の文字が左角括弧であるか確認する
            else if (scanner.Check(IsLeftBracket))
                //配列を構文解析する
                return ParseArray(scanner);
            //現在位置の文字が二重引用符であるか確認する
            else if (scanner.Check(IsDoubleQuote))
                //文字列を構文解析する
                return ParseString(scanner);
            //現在位置の文字が符号であるか確認する
            else if (scanner.Check(IsSign))
                //数値を構文解析する
                return ParseNumber(scanner);
            //現在位置の文字が数字であるか確認する
            else if (scanner.Check(IsDigit))
                //数値を構文解析する
                return ParseNumber(scanner);
            else
                //予約語を構文解析する
                return ParseWord(scanner);
        }

最後にJSONの構文解析を行う関数を追加します。値を構文解析する関数を呼び出すだけです。

        //JSONを構文解析する
        public static object Parse(Scanner scanner)
        {
            //値を構文解析する
            return ParseValue(scanner);
        }

使用例

次のように使用することができます。

            string json = "{\"name\": null, \"age\": 25, \"isMale\": true, \"hobby\": [\"mathematics\", \"programming\"]}";
            Dictionary<string, object> osDict = JsonParser.Parse(new Scanner(new Text(json))) as Dictionary<string, object>;
37
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
36