kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

NParsec+ …で Excel を…CLI…(3) NParsec で Tokenizer をTDD


ってわけで、Pattern での切り出し範囲とかをTDDしたら次は Tokenizer です。

Tokenizerの役割としては Pattern で指定された範囲を Parser にとって意味のある形式にすることと、その分類のタグ付をすることです。

さて、Pattern の記述はちゃんと仕様を見ましたか、Excelの字句仕様をちゃんと見てPatternを書いてる人は以下の numeric-constant の仕様が引っかかるに違いありません。

見たい人は ECMA のサイトからダウンロードできるドキュメントにありますんで、ダウンロードしましょう。(Standard ECMA-376)

式の字句構造の詳細は ECMA-376 1st edition Part 4 にあります。3.17 Formulas-3.17.2 Syntax がその定義です。

という訳で Excel の変態っぷりとおつきあいするのが始まりです。

numerical-constant:
    whole-number-part [ . ] [ exponent-part ]
    . fractional-part [ exponent-part ]
    whole-number-part . fractional-part [ exponent-part ]

Excelの式では字句仕様として 小数点から始まる浮動小数点表記を認めるんですね。また、10.e2 等、少数点下に数字がなく、いきなり exponent-part が現れるパターンを許しています。

また NParsecデフォルトの exponential の実装は

    public static Pattern IsExponential () {
        return Patterns.Sequence (
            Among (new char[] { 'e', 'E' }),
            IsChar ('-').Optional(),
            IsInteger ());
      }

となってます。一方 Excel は

exponent-part:
  e [ sign ] digit-sequence
  E [ sign ] digit-sequence

sign:
  +
  –

これもあいませんね、 + を許さなければいけないです。

これは上記をもろもろ踏まえてパターンを書くとこうなります。

        internal static readonly Pattern digits = Patterns.InRange('0', '9').Many(1);
        internal static readonly Pattern exponentPart = Patterns.Sequence(
              Patterns.Among('e', 'E'),
              Patterns.Among('+', '-').Optional(), digits);
        internal static Pattern wholeNumberPart = Patterns.InRange('0','9' ).Many(1);
        internal static Pattern numericalConstant =
              Patterns.Or(Patterns.Or(
                  Patterns.Sequence(digits, Patterns.IsChar('.').Optional(), exponentPart.Optional()),
                  Patterns.Sequence( Patterns.IsChar('.'),digits, exponentPart.Optional())),
                  Patterns.Sequence(digits, Patterns.IsChar(','), digits, exponentPart.Optional()));

んで、こういう変態文字列を CLR の double.ToString に与えた場合が怖いので、 numberTokenizer を実装します。

        internal static Tokenizer numberTokenizer =
              (src, begin, len) =>
                  {
                      string substring = src.Substring(begin, len);
                      if (src[begin] == '.') substring="0"+substring;
                      substring = substring.Replace(".e", ".0e");
                      return double.Parse(substring);
                  };

ドウセ短い文字列なのでコピーとか気にしない。って実装ですが神は細部に宿るんで気になる人はもっといい実装考えてください。

という訳で、こいつはフィールドにラムダが入ってるだけなんで、テストメソッドからガンガン呼んでテストできますね。Pattern と Tokenizer はこんな感じでTDD進められます。

Assert.AreEqual( (double)12.0, numberTokenizer( “aaa 12”,4,2 ));

その他 Excel が許す物を文字列として書いてパース結果が期待値とあうかをテストすればOKですね。パターンマッチ結果をもらう部分なんで、パターンが match しないような変な文字列での挙動をテストする必要性はありません。

んでこの状態で ExpressionTree な実装おかしくなりましたよね。

pNumber が OnDecimal のままだと doubleに反応しないからです。OnDecimal でやっていた pNumber の文字列変換処理は Tokenizer に移っていますので、 pNumber の定義を以下に書き換えます。

        private static readonly Grammar pNumber = Terms.OnToken<double,Expression>(
              (from, len, data)=>Expression.Constant( data ));

これであなたのパーサーは Excel が受け入れる形式の浮動小数点値を受け入れて計算するパーサーになりました。

後は文字列リテラル、boolリテラル等のリテラル処理をちゃんとするのと、識別子とか(セル範囲の文字列とか、関数名)をパターンを用意して取り出すようにすると Excel 式のパースの前半、Lexical Parsing (Scanner/Tokenizer) としてはデリミタを除いて実装完了になります。

次回は Lexical Parsing のまとめとして、Lexeme とデリミタの処理についてです。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。