C#で簡易テンプレート処理2007年09月17日 02時01分31秒

いきなりC#ネタ

ZF入門も書かずに脈絡なく。しかも.NET 1.1。ごめん、がんばる。

ありそうで見つからないので

ここしばらく久しぶりに仕事でC#を使っている。まぁだいぶ勘も戻ってきて、「やっぱ慣れてる言語はよいなー」とか思っていたのだが、ちょっと欲しい機能が見つからなかった。

いま実装中の機能で、DataSetにキャッシュしたデータをもとにUPDATE用のクエリを生成するのだが、状況によってすこしだけ異なるクエリを生成し分ける必要にせまられ、簡単なテンプレートエンジンが欲しくなったのだ。

ひょっとするとすでに優秀なライブラリがあるかもしれない(実はあとでNVelocityなんてのがあることに気づいた)が、探して評価している時間もないので割り切った機能で自作をしてみた。

仕事で作ったやつなのでネタにするのはちょっと抵抗あるが、仕様検討も含めて1時間かかるかかからないかくらいの規模だし、どうせ誰かが似たようなやつ作ってるだろうしでソースを公開してみることにした。需要あるかはわからんけど。

仕様

ってほど機能があるわけではないが、標準のString.Formatのインデックスベースのプレースホルダの代わりにIDictionaryのキーやDataColumnのカラム名を使うように考えた。要はこちらで紹介されている「JSON - String.prototype.supplant」みたいな記法だ。

たとえば

string template = "name={Name}, value={Value}";
みたいなテンプレートがあって、これに
  • "Name", "Value"というColumnがあるDataTableのDataRow
  • "Name", "Value"というキーを持つIDictionary(HashtableとかNameValueCollectionとか)
を与えるって使い方。 その他はなんの付加価値もない。条件分岐も繰り返し処理もサブテンプレートの差込もできない。

ただし、パラメータ名の表記は後ろにコロン+フォーマット指定ができるように展開するため、String.Formatと同様にフォーマット指定できるようにしてみた。

仕組み

ソースみればだいたいわかると思うが、以下のようなプロセスで展開する。

  1. {}で囲まれたパラメータ名を抽出順にリストにし、そのインデックス番号でもってテンプレート内のパラメータ名を置換する
  2. 展開時はパラメータ名のリストを順次走査して与えられたDataRowやIDictionaryから値を抽出してobject[]を作成、先に変換したテンプレートを用いてString.Formatする
こんな感じでかなりシンプル。

ソース

このくらいの感じ。例によってNYSLってことで。

using System;
using System.Collections;
using System.Data;
using System.Text.RegularExpressions;

namespace DaraJ {
  public class TemplateProcessor {
    // テンプレートソース
    protected string _source;
    
    // コンパイル済みソース
    protected string _compiledSource;
    
    // テンプレート変数名のリスト
    protected ArrayList _varNames;
    
    // Regex.Replace用のMatchEvaluator
    protected MatchEvaluator _evaluator;
    
    // テンプレート変数抽出用正規表現
    protected Regex _variableParser;
    
    // コンパイル済みフラグ
    protected bool _compiled;
    
    // デフォルトコンストラクタ
    public TemplateProcessor() : this(null) {
    }
    
    // テンプレートソースを指定するコンストラクタ
    public TemplateProcessor(string source) {
      this._variableParser = new Regex( @"\{([a-zA-Z_]\w*)(:.+?)?\}" );
      this.Source = source;
      this._evaluator = new MatchEvaluator( this.evaluateVariable );
    }
    
    // テンプレートソースを取得・設定
    public string Source {
      get {
        return this._source;
      }
      set {
        if( value == this._source ) return;
        this._source = value;
        
        this._compiledSource = null;
        this._varNames = new ArrayList();
        this._compiled = false;
      }
    }
    
    // コンパイル済みソースを取得
    public string CompiledSource {
      get {
        return this._compiledSource == null ? String.Empty : this._compiledSource;
      }
    }
    
    // テンプレート変数名リストを取得
    public string[] VariableNames {
      get {
        return (string[])(this._varNames.ToArray( typeof(string) ));
      }
    }
    
    // コンパイル済みか
    public bool Compiled {
      get {
        return this._compiled;
      }
    }
    
    // テンプレートをコンパイル
    public void Compile() {
      this._varNames = new ArrayList();
       this._compiledSource = this._variableParser.Replace( this.Source, this._evaluator );
      this._compiled = true;
    }
    
    // テンプレートソースを指定してテンプレートをコンパイル
    public void Compile(string source) {
      this.Source = source;
      this.Compile();
    }
    
    // IDictionaryをパラメータにしてテンプレート処理を実行
    public string Exec(IDictionary parameters) {
      if( ! this.Compiled ) this.Compile();
      
      ArrayList paramList = new ArrayList();
      foreach(string varName in this._varNames ) {
        paramList.Add( parameters[ varName ] );
      }
      return String.Format( this.CompiledSource, paramList.ToArray() );
    }
    
    // DataRowをパラメータにしてテンプレート処理を実行
    public string Exec(DataRow parameters) {
      if( ! this.Compiled ) this.Compile();
      
      ArrayList paramList = new ArrayList();
      foreach(string varName in this._varNames ) {
        paramList.Add( parameters[ varName ] );
      }
      return String.Format( this.CompiledSource, paramList.ToArray() );
    }
    
    // Regex.Replaceから呼び出される置換メソッド
    // テンプレート変数名を出現順序のインデックスに置換しつつ
    // テンプレート変数名をリストに追加する
    private string evaluateVariable(Match m) {
      int index = this._varNames.Count;
      string varName = m.Groups[1].ToString();
      
      this._varNames.Add( varName );
      
      return String.Format( "{{{0}{1}}}", index, m.Groups[2] );
    }
  }
}
で、使い方はこんな感じ。
using System;
using System.Collections;
using System.Data;

namespace DaraJ {
  public class TestClass {
    public static void Main() {
      TemplateProcessor template = new TemplateProcessor();
      // テンプレートソースを設定
      template.Source = @"{ItemName} \{Price:#,##0} (\{UnitPrice:#,##0} x {ItemNum})";
      
      // IDictionaryで実行
      Console.WriteLine( template.Exec( createHashtable() ) );
      
      // DataRowで実行
      foreach(DataRow row in createTable().Rows) {
        Console.WriteLine( template.Exec( row ) );
      }
    }
    
    // テストデータのDataTableを作成
    private static DataTable createTable() {
      DataTable table = new DataTable();
      table.Columns.AddRange( new DataColumn[] {
        new DataColumn( "ItemName", typeof(string) ),
        new DataColumn( "UnitPrice", typeof(decimal) ),
        new DataColumn( "ItemNum", typeof(int) ),
        new DataColumn( "Price", typeof(decimal), "UnitPrice * ItemNum" )
      } );
      foreach(object[] data in new object[][] {
        new object[] { "HDD (40GB)", 1980, 1 },
        new object[] { "ボールマウス", 400, 3 },
        new object[] { "キーボード (白)", 980, 3 }
      } ) {
        DataRow row = table.NewRow();
        row.ItemArray = data;
        table.Rows.Add( row );
      }
      
      return table;
    }
    
    // Hashtableのテストデータを作成
    private static Hashtable createHashtable() {
      Hashtable hash = new Hashtable();
      
      hash["ItemName"] = "HDD (80GB)";
      hash["UnitPrice"] = 5000;
      hash["ItemNum"] = 2;
      hash["Price"] = 10000;
      
      return hash;
    }
  }
}

改造など

いまのとこIDictionaryとDataRowの受け取りをオーバーロードで実装しているため、メソッドシグニチャを除けばまったく同じコードでかなりみっともないが、TypeをキーにしたHashtableに、テンプレート変数名から値を取り出すメソッドをくるんだdelegateを登録するように改造したりすれば他の型(ってちょっと思いつかないが)への対応もむずかしくないかと。

でも、NVelocityを先に見つけてりゃこんなんつくらんかったな。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※なお、送られたコメントはブログの管理者が確認するまで公開されません。

名前:
メールアドレス:
URL:
コメント:

トラックバック

このエントリのトラックバックURL: http://dara-j.asablo.jp/blog/2007/09/17/1802125/tb

※なお、送られたトラックバックはブログの管理者が確認するまで公開されません。