そろそろ Metro Design について考えてみよう

どうやら「Metro UI」が「Modarn UI」に切り替わってきているので、ワタクシとしてははれて Metro Design について再考してみようということで、少し書き下し。

東京メトロ UI | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2907

なところで書きましたが…って書いてないか。Community Open Day 2012 だったか直前の.NETラボ勉強会でちらっと話しましたが「Metro UI」もとのコンセプトは地下鉄の標識デザインの「Metro」です。なので、Subway UI でもよいし、Tube UI でも良かったのですが、Metro UI ということになった模様です。この Metro UI

Japanese Dee//Party and Bullshit//

2010年10月のブログ|Japanese Dee//Party and Bullshit// より拝借

や、東京メトロの標識を見ると分かりますが、「単色に白抜き文字」あるいは「単色に黒文字」が普通です。文字に白を使うか黒を使うかは、背景に使われる色の明度により決まるわけです。東京メトロの路線図をみると、表示されている線の色と路線で使われているマークの色が共通になっています。

また、色自体は「白っぽいパステルカラー」ではなく「原色でもなく」、東京メトロの少し明度の高い淡い色を使って統一されています。ニューヨークの地下鉄の場合はビビッドめな色を使っていますが、すくなくとも全体のカラーは統一されているわけです。

さて、当時の Metro UI では盛んに「タイリング」が協調されていましたが(多分、Modarn UI もそうなると思いますが)、元ネタの Metro Design を見るとわかるように、特にタイリングを気にしているわけではありません。いや、格子状の配置を気にしてはいるものの、それはグリッドシステムを使っているデザインパターンがあります。

道路標識をあらためて見ると分かりますが、縦横の補助線を入れると確かにグリッド(格子状)になっています。ただし、デザインの本を読むと分かるのですが、文字によっては横幅を調節しないと、「きちんと格子状に見えない」という調節が必要になります。つまり、単純に格子状に文字を配置したからといって、文字が格子状に見えない。だから、左右の空白を調節して格子状でなくすることによって、格子状に見えるようにする。という矛盾を抱えています。

これを、Microsoft 社が出す、デザインガイドラインに当てはめると「文字や図形のマージンを固定値に取る」ことによって「格子状に配置する」ことを目指していますが、実際作ってみると「格子状には見えない」という現象が発生するのです。そうなると、美しいデザインするためには、あえて「格子状に配置をしない」という工夫が必要なわけです。ジレンマですね(苦笑)。

格子状(グリッド)の問題についてはさておき、おそらく Microsoft 社としては Metro UI の裏の指針を以下のようにすべきだったと思っています。

  • 格子状の配置は、解像度やディスプレイサイズが異なる環境において、高速に再配置が可能である。
  • Metro UI 風のアイコンやサムネール画像は、高速に描画するために重要である。
  • Apple の 3D デザインと、意匠特許の面で将来的に争わずに済む。

の3点です。

■グリッドの配置は、高速に再配置が可能

格子状に画像や文章を配置すると、ブラウザの文章の配置のように再配置が非常に高速にできます。CSS の margin や padding のように外側と内側の空白をあらかじめ準備しておくことによって、位置が変わっても計算が楽です。もちろん、スプライン曲線に文字を沿わせるなどのデザインテクニックもあるのですが、機械的に「綺麗に見えるような配置」をするためには、grid にしてしまう方がコーディングもスピードも楽なのです。

そのあたりのデザインを省力化するためにグリッドの配置は必要、ということですね。これは Mordarn UI にも当てはまることです。ただし、先の「格子状の配置のジレンマ」で示したように、格子状に配置したからといって「美しいデザイン」になるとは限らないので、そのあたり、デザインとしては一歩も二歩もダサいデザインになってしまいます。逆に言えば、他の modarn application と差別化するためには、「格子状に配置しているように見えるけれども、実は格子状ではない」というタイポグラフィのデザインテクニックを使うと良いでしょう。

■アイコンやサムネール画像を単色化する

かつて Aero と言う「半透明な」ウィンドウスタイルを Microsoft 社は「美しい」ものとしていましたが、現在は Mordan UI の単色のアイコンを「美しい」と言っています。「美しい」の定義はさておき、Apple が使う 3D & グラス処理は確かに「美しい」アイコンを作りますし、作業に適したアイコンとなっています。モノクロ時代の Classic Apple も良いのですが、まあ「クリエイティブ」な感じがすることは確かです。

この 3D とグラス処理、角が丸い処理のために Apple の画像処理は非常にコストをかけています。おそらく、計算で行っているのではなくて内部的にいくつかのパターンを持つという荒業をしているような気がしますが、まぁ、高速に動きます。すごいです。実は、スライドや回転については、一見「高速」に動いているように見えますが、実は違います。このあたりは、「高速に見える」ことを重視している Apple らしいです。

さて、Microsoft 社の Windows の場合は、Apple ほど高速には動きません。これは技術力の問題ではなくて、Apple の場合は、iPhone などのハードウェアに特化したコードを書けばよいのですが、Microsoft の場合は、各社の画像ボードの性能に依存するという問題があるためです。このため、非力なボードを使っていたり、グラフィックボードのドライバーがいまいちだったりすると、このグラス処理を高速におこなうことができません。

そこで、ひとつの方法として、アイコンやサムネール画像を「単色」あるいは「減色」してしまうことで低機能なグラフィックボードであっても高速に表示ができます。グラス処理や 3D 動作、角がまるい処理も外してしまえば、どんどん高速になるはずです。また、色を減らせば画像サイズも減るわけですから、インターネット上から直接ダウンロードしたり HDD から読み込んだりするスピードも速くなる(実際は、大量な小さなファイルは遅くなるのですが)可能性があります。画像をメモリ上に置いてもサイズが小さくなりますね。

という訳で、単色パターンにすると Windows のスタートメニューやアプリメニューなどの動作が早くなるハズなのです。が、まぁ、実際は XAML の描画ルーチンが駄目なまま(おそらくGDI+のままかと)なので、全然高速ではありませんがね。

■アイコンの意匠問題を避ける

アイコンの 3D 化はフルカラー表示、グラス表示を続けると、おそらく Apple と Microsoft のアイコンは非常に似通ってしまいます。Android のアイコンを見て分かる通り、極めて iPhone のアイコンに似ています(似せているというもあるけど)。また、Linux の Desktop アプリも Windows アイコンに似てますよね(これも似せているというもあるけど)。

アイコンや画像の意匠に関しては、デザイナの問題もありますが、OS を提供する側としては「Apple のデザインに似る」というのは避けたいところです。Window システム自体が Apple の真似なんだからどうでもよかろう、MS-DOS 自体が Unix の真似なんだから、今更。という感も無くもないのですが、だんだんとパイが固定になっている OS 業界で「意匠」に引っ掛かるのは避けたいところなのです。

となれば、Apple が提唱する 3D & グラス路線、とは真っ向から対立する「単色アイコン & グリッド配置」をキャンペーン化してしまえばよいのです。デザインの路線としては、「Metro Design」があるので、これが「実に読みやすい、コンテンツ主導のデザインである」と宣伝すればよいわけです。まあ、けばけばしい看板よりは、コンテンツが読みやすいでしょうが、

Apple の 3D & グラス路線も別に「コンテンツを侵害している」訳ではないんですけどね。むしろ、Windows XP のおもちゃのようなデザイン/色彩感覚が、創造力をげんなりさせていた訳で、Visual Studio のツールバーのアイコン表示はやっとこさ VS 2008 あたりで完成されたかと思いきや(ぱっとみて、どの機能か分かる位「道具化」している)、VS 2012 になって単色系アイコンになり「道具」を取り去られた感じなんですけどね。Visual Studio のリソースを切り替えるツールでも作りますか(真面目に)。

それはともかく、3D & グラス路線を目指さなくなったので、アイコンデザインとしては Apple と意匠戦争にはならなくなります。また「これが Microsoft 社が提供する次世代のデザインだ」と定義することで、自分がトップに立てる(自分で土俵を作ったからね、当たり前)というおまけもついてきます。一応、Modarn UI の前哨戦としては成功しているみたいですね。Google アナリシスの UI も Modarn UI 風にカラーが変わっていましたから。github は、だいぶ前に追随したし。

そんな訳で、Metro UI についてぼちぼちと裏側を推測してみましたが、じゃあ本来の Metro Desgin の場合はどうなるのか?っていうのは、また後日。

カテゴリー: windows 8 | 1件のコメント

[C++] 試しにデクリメント付きの bool 型を実装してみる

VC++2010 の bool 値の動きが、false -> true -> true なのだから、それに合わせてデクリメントを作ってみる。

class BOOL
{
	bool _b ;	// 内部でブール値を持つwww
public:
	BOOL( bool b = false ) {
		_b = b;
	}
	// bool型へキャスト
	operator bool() {
		return _b ;
	}
	// bool型から代入
	BOOL& operator =( bool b ) {
		_b = b;
		return *this;
	}
	// 前置インクリメント
	BOOL& operator ++() {
		_b = true;
		return *this;
	}
	// 後置インクリメント
	BOOL operator ++(int) {
		BOOL b; b._b = _b;
		_b = true;
		return b;
	}
	// 前置デクリメント
	BOOL& operator --() {
		_b = false;
		return *this;
	}
	// 後置デクリメント
	BOOL operator --(int) {
		BOOL b; b._b = _b;
		_b = false;
		return b;
	}
};

内部的に、bool 値を持っているのは GAG というかなんというか、意外と詰まらないコードになってしまいました。
最初は b |= 1; b &= 0; なことをやるつもりだったのですが、実は 0/1 しか値を取らないのだから、直接 false/ture を入れてしまえば良いわけです。

■結果

	// デクリメントが可能なbool値
	BOOL bl = false;
	cout << ++bl << endl;
	cout << --bl << endl;
カテゴリー: C++ | [C++] 試しにデクリメント付きの bool 型を実装してみる はコメントを受け付けていません

[C++] bool値をインクリメントすると、ture/false を繰り返す理由…をこじつける

bool値をインクリメントする……? – Togetter
http://togetter.com/li/356718
c++ – bool operator ++ and — – Stack Overflow
http://stackoverflow.com/questions/3450420/bool-operator-and

3.2 Increment and decrement [expr.pre.incr]
1 The operand of prefix ++ is modified by adding 1, or set to true if it is bool (this use is deprecated).
The operand shall be a modifiable lvalue. The type of the operand shall be an arithmetic type or a pointer
to a completely-defined object type. The value is the new value of the operand; it is an lvalue. If x is not
of type bool, the expression ++x is equivalent to x+=1. [Note: see the discussions of addition (5.7) and
assignment operators (5.17) for information on conversions. ]

を見て「えーッ!!! インクリメントすれば true/false を繰り返すほうが対称性が高いし、他との互換性も高いッ!!! 当然の規約だろう。昔 bool 型が int で実装されていたのはマシンの制約に関わる部分が多くて、コーディング的には、true/false が交互になるほうがよい」というのをぴきーんと考えたのですが…タイトルを見て分かるように「こじつけ」です。

実は手元にある VC++2010, VC++2012, G++4.5.2 で試したところ、bool をインクリメントすると true(1) -> ture(1) なんですわー、というわけで「非推奨」ですね、つーか、処理系に依存するコードを書いちゃダメですね。

■実験コードを作る

int main( void )
{
	bool b = false;

	// 後置型
	cout << "bool: " << b++ << endl;	// false
	cout << "bool: " << b++ << endl;	// true
	cout << "bool: " << b   << endl;	// false
	cout << endl;

	b = false;
	// 前置型
	cout << "bool: " << ++b << endl;	// true
	cout << "bool: " << ++b << endl;	// false
	cout << "bool: " << b   << endl;	// true
	cout << endl;

	b = false;
	for ( int i=0; i<5; i++ ) {
		cout << b << endl;
		b = !b ;	// 反転
	}
	cout << endl;
	// 利点としてはインクリメントを使うと1行で書ける
	b = false;
	for ( int i=0; i<5; i++ ) {
		cout << b++ << endl;	// 後でインクリメント
	}
	cout << endl;

	return 0;
}

期待するところは、コメントにある通り true/false を繰り返します。
期待通りに動くのであれば、「!b」で反転させるよりも、b++ にすると1行で済むことが分かります。

bool 値は、一見「論理式」のために用意されている on/off の状態を示すように見えますが、。反転を表す「!」演算子を使う場合 on <-> off の交互になる、つまりは、

  • on であれば off になる
  • off であれば on になる

のようになります。しかし bool 値を一般的な int 型と考えてみると、int型のように 0 -> 1 -> 2 -> … -> MAX -> 0 のように最大値を過ぎたら 0 に戻るとうのが普通なのです。となると、0,1 の値しか持たない=1ビットで表すことができる数値として bool をとらえると、

  • 0 に 1 を加えて 1 になる
  • 1 に 1 を加えて 0 になる

という論法になります。この考え方は、欧米の家電のスイッチが「0」と「1」になっていることから分かりますね。

  • スイッチが入っいない状態=0の状態
  • スイッチが入っている状態=1の状態

という訳です。

■3値のクラスを作る

さて、この論法を確認するために 3値のクラスを考えてみましょう。

// 例えば3値のクラスを作る
class Value
{
	int _v;
public:
	Value() : _v(0) {}
	// 前置
	Value& operator ++() {
		if ( ++_v >= 3 ) _v=0;
		return *this;
	}
	// 後置
	Value operator ++(int) {
		Value v;
		v._v = this->_v;
		if ( ++_v >= 3 ) _v=0;
		return v;
	}
	// int型へキャスト
	operator int() {
		return _v;
	}
};
ostream& operator << ( ostream &s, Value v )
{
	switch ((int)v) {
	case 0: s << "one" ; break;
	case 1: s << "two" ; break;
	case 2: s << "three" ; break;
	}
	return s;
}

0, 1, 2 の3つの値を取る整数値の場合には、++演算子は普通に使える、というのが期待されます。

	// 3値クラスを使う
	Value v ;
	cout << v++ << endl;		// one
	cout << v++ << endl;		// two
	cout << v++ << endl;		// three
	cout << v++ << endl;		// one
	cout << endl;

これは期待通りに、one -> two -> three -> one のように動きます。
チェックボックスの値も、unenabled -> off -> on -> unenabled のように動くことが期待できるわけです(まあ、内実がint型だからというのもありますが)。

■任意の値を最大値とするテンプレートを作る

3値ではなくて、任意の値「MAX」が取れるようにテンプレートクラスにします。

// 任意の値までのテンプレートクラス
template<int MAX>
class TValue
{
	int _v;
public:
	TValue<MAX>() : _v(0) {
	}
	// 前置
	TValue<MAX>& operator ++() {
		if ( ++_v >= MAX ) _v=0;
		return *this;
	}
	// 後置
	TValue<MAX> operator ++(int) {
		TValue<MAX> v;
		v._v = this->_v;
		if ( ++_v >= MAX ) _v=0;
		return v;
	}
	// int型へキャスト
	operator int() {
		return _v;
	}
};

この場合も期待通りに動きます。MAX の次が 0 なので、サイクリックな値として使えますね?

	TValue<3> v3 ;
	cout << v3++ << endl;		// one
	cout << v3++ << endl;		// two
	cout << v3++ << endl;		// three
	cout << v3++ << endl;		// one
	cout << endl;

■Bool へ typedef する

先のテンプレートに MAX=2 を指定すると、ほら、規約にある bool と同じ動作をしますね。

	typedef TValue<2> Bool;		// 2値のBoolクラス
	Bool bb;
	cout << bb++ << endl;		// 0
	cout << bb++ << endl;		// 1
	cout << bb++ << endl;		// 0
	cout << endl;

つまりは、bool 型というのは、true/false という特別な型ではなくて、int 型や char 型と同じように「最大値を指定してサイクリックにインクリメントできる型」の特殊なものして、定義できる訳です。これはなんか「数学的」で綺麗でいいですよね。

■で、最初に戻って bool 型をインクリメントすると

さて、ここまで薀蓄を含めて bool 値の考察をしていきましたが、実装はどうなっているでしょうか?

	bool b = false;

	// 後置型
	cout << "bool: " << b++ << endl;	// false
	cout << "bool: " << b++ << endl;	// true
	cout << "bool: " << b   << endl;	// false
	cout << endl;

	b = false;
	// 前置型
	cout << "bool: " << ++b << endl;	// true
	cout << "bool: " << ++b << endl;	// false
	cout << "bool: " << b   << endl;	// true
	cout << endl;

の結果は、VC++2010, VC++2012 で実行すると、

bool: 0
bool: 1
bool: 1

bool: 1
bool: 1
bool: 1

あーあー、うん、あーあ、どうでもいいや、ってな気分です。

# ちなみに、VC++2010 の場合はデクリメントをしようとするとコンパイルエラーになります。

処理としては、0 -> 1, 1 -> 1 という実装みたいですね。

もしデクリメントを実装するとすると、1 -> 0, 0 -> 0 が素直かと。

 

カテゴリー: C++ | [C++] bool値をインクリメントすると、ture/false を繰り返す理由…をこじつける はコメントを受け付けていません

[C#] XDocument から XmlNavigater を使えるように拡張

巷では、Windows 8 RTM, Visual Studio 2012 RTM で盛り上がっていますが、しばらくは平常運転で。
ちなみに 90日試用版は以下からダウンロードできます。

Download Windows 8 Enterprise Evaluation
http://msdn.microsoft.com/ja-jp/evalcenter/jj554510.aspx

# 英語のページだけど、きちんと日本語版がダウンロードできます。2.4GB 程度です。
# MSDN のほうも Japanese 版が用意されています。

さて、本題の XmlDom のほうはテストルーチンから作っていきます。
内部的には XML文字列 -> XDocument -> XmlDocument -> XmlNavigator の経由で渡しています。
ちまちま XML文字列をパースするコードを書いてもいいのですが、パース自体はあまりスピードを問わないので。
# 実は、System.Xml.Linq は Silverlight に含まれていない(今は含まれている?)ので避けていたのですが、
# Siverlight 自体が終了しそうな雰囲気なので、XDocument を使うことに決定

■テストコードを書く

以前に書いた XNode, XmlNode 構築のコードを XML文字列に直します。
Assert.AreEqual 部分はそのまま動くはず…というか、動かないと駄目です。

namespace TestXmlDom
{
	[TestClass]
	public class TestXmlDocument
	{
		[TestMethod]
		public void TestParse1()
		{
			string xml = "<root><name>masuda tomoaki</name></root>";
			var doc = new XmlDocument(xml);

			var q = new XmlNavigator(doc)
				.Where(n => n.TagName == "name")
				.FirstOrDefault();

			Assert.AreEqual("masuda tomoaki", q.Value);
		}

		[TestMethod]
		public void TestParse2()
		{
			string xml = @"
<root>
 <name>masuda</name>
 <name>yamada</name>
 <name>yamasaki</name>
</root>";
			var doc = new XmlDocument(xml);
			var q = new XmlNavigator(doc)
				.Where(n => n.TagName == "name")
				;
			
			Assert.AreEqual(3, q.Count());
			Assert.AreEqual("masuda", q.First().Value);
		}

		[TestMethod]
		public void TestParse3()
		{
			string xml = @"
<root>
 <person id='1'>
  <name>masuda</name>
  <age>44</age>
 </person>
 <person id='2'>
  <name>yamada</name>
  <age>20</age>
 </person>
 <person id='3'>
  <name>tanaka</name>
  <age>10</age>
 </person>
</root>";
			var doc = new XmlDocument(xml);
			var q = new XmlNavigator(doc)
				.Where(n => n.Attrs["id"] == "2")
				.FirstOrDefault();
			Assert.AreEqual("person", q.TagName);
			// ExDoc記述
			Assert.AreEqual("yamada", q / "name");
			Assert.AreEqual("20", q / "age");
		}
	}
}

XNode じゃなくて、XmlNode を使う理由としては「q / “name”」という書き方ができるからです。
この部分はXElemnetを使って「q.Element(“name”).Value」と書いても同じです。

ただし、q 自体は XNode を返すので Element() を含んでいなくていまいち不便なんですよね。

■XDocument から XmlDoucmnet を構築する

インターフェースとして、XmlDocument を用意します。
単純に、XDocument の中身を XmlDocument に移し替えているだけです。

ざっとインターフェースを合わせていきます。

namespace Moonmile.XmlDom
{
	public class XmlDocument : XmlNode
	{
		public XmlNode documentElement { 
			get
			{
				if (this.Children.Count() > 0)
				{
					return Children[0];
				}
				else
				{
					return XmlNode.Empty;
				}
			}
		}

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public XmlDocument() 
		{
		}

		/// <summary>
		/// コンストラクタ(XML文字列で初期化)
		/// </summary>
		/// <param name="xml"></param>
		public XmlDocument( string xml)
		{
			this.LoadXml(xml);
		}

		/// <summary>
		/// ファイルから構築する
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		public XmlDocument Load(string path)
		{
			return this.Load(XDocument.Load(path));
		}

		/// <summary>
		/// XML文字列から構築する
		/// </summary>
		/// <param name="xml"></param>
		/// <returns></returns>
		public XmlDocument LoadXml(string xml)
		{
			StringReader sr = new StringReader(xml);
			return this.Load(XDocument.Load(sr));
		}

		/// <summary>
		/// XDocumentオブジェクトから構築する
		/// </summary>
		/// <param name="doc"></param>
		/// <returns></returns>
		public XmlDocument Load(XDocument doc)
		{
			var root = LoadXNode(doc.FirstNode);
			this.Children.Add(root);
			return this;
		}

		protected XmlNode LoadXNode(XNode node)
		{
			var nn = new XmlNode(node.TagName(), node.Value());
			if (node.NodeType == System.Xml.XmlNodeType.Element)
			{
				var el = node as XElement;
				foreach (var at in el.Attributes())
				{
					nn.Attrs.Add(new XmlAttr(at.Name.ToString(), at.Value));
				}
				foreach (var n in el.Nodes())
				{
					nn.Children.Add(LoadXNode(n));
				}
			}
			return nn;
		}
	}
}

という訳で、これで XML を自由に比較的自由に扱うことができました。
今度は HTML 文字列を扱えるように、HtmlNode, HtmlDocument へ移していきます。

カテゴリー: C#, XmlDom | [C#] XDocument から XmlNavigater を使えるように拡張 はコメントを受け付けていません

フリーなプログラマ、デザイナを緩く募集します

具体的なところは以下から

募集 | Moonmile Solutions Blog
http://www.moonmile.net/blog/recruiting

以前から考えていたのですが、少し具体的に動かそうと思いブログに公開します。

  • 依頼を受けるものが、直近かつ短期間のものが多い。
    →  ソフトウェア会社には頼みにくい。
    → 私自身のキャパでは無理。
  • そこそこ、仕事の幅がある(詳細設計がないなど)ものが多い。
    →  単なるコーダーでは駄目。
  • 中間マージンが無ければ、そこそこ収入が良い。
    → 「派遣」を通すと、マージン分だけ損してしまうので、馬鹿馬鹿しい。

というパターンが多いので、もう少し「私が紹介できるプログラマ、デザイナな方」を広く緩く募集ところです。

カテゴリー: 仕事, 雑談 | フリーなプログラマ、デザイナを緩く募集します はコメントを受け付けていません

[C#] XNode を使って XNavigator を作る

[C#] XElement とは違う LINQ できる XmlNode を作成する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3628

なところで、自前の XmlNode を使って LINQ 操作を実現した訳ですが、XNode との重複が激しいので、試しに System.Xml.Linq.XNode http://msdn.microsoft.com/ja-jp/library/system.xml.linq.xnode.aspx を使って書き直してみます、っていう実験を。

■XNavigatorを実装。

自前の XmlNavigator をそのままコピペして実装し直します。
いくつかのメソッド名を変更しますが、そのままですんなりコンパイルが通ります。

public class XNavigator : IEnumerable<XNode>
{
	XNode _root;

	public XNavigator(XNode root)
	{
		_root = root;
	}

	public IEnumerator<XNode> GetEnumerator()
	{
		return new Enumerator(_root);
	}

	System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
	{
		return new Enumerator(_root);
	}

	protected class Enumerator : IEnumerator<XNode>
	{
		XNode _root;
		XNode _cur;

		public Enumerator(XNode root)
		{
			_root = root;
		}

		public XNode Current
		{
			get { return _cur; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _cur; }
		}

		public bool MoveNext()
		{
			if (_cur == null)
			{
				_cur = _root;
				return true;
			}
			if (_cur.NodeType == System.Xml.XmlNodeType.Element &&
				((XElement)_cur).Nodes().Count() > 0)
			{
				_cur = ((XElement)_cur).FirstNode;
				return true;
			}
			if (_cur.NextNode != null)
			{
				_cur = _cur.NextNode;
				return true;
			}
			XNode cur = _cur;
			while (true)
			{
				XElement pa1 = cur.Parent;
				if (pa1 == null)
				{
					_cur = null;
					return false;
				}
				if (pa1.NextNode != null)
				{
					_cur = pa1.NextNode;
					return true;
				}
				cur = pa1;
			}
		}

		public void Reset()
		{
			_cur = null;
		}
	}
}

■テストコードを書く

XmlNavigator 用に書いたテストコードを XNavigator 用に直します。

  • XML 構築のところは、XDocument を使って書き換え。
  • XNode のままでは Where メソッドでうまく動かないので、ちょっと思案

という具合です。

XNode のままだと、実は Nodes コレクションとかが無いんですよね…このあたり、DOM の頃からそうなのですが、インターフェースを重視するあまり、利用する時に XElement にキャストをしないといけないという罠があります。
XNavigator 自体は、XElement, XText などのオブジェクトも返すので、この TagName や Value 当たりが鬼門となります。

	[TestClass]
	public class TestXNavi
	{
		[TestMethod]
		public void TestNormal1()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XElement("name", "masuda"))));

			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == "name")
				.FirstOrDefault();
			Assert.AreEqual("masuda", q.Value());

		}
		
		[TestMethod]
		public void TestNormal2()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XElement("name", "masuda"),
						new XElement("name", "yamada"),
						new XElement("name", "yamasaki"))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == "name")
				.Select( n => n );

			Assert.AreEqual(3, q.Count());
			Assert.AreEqual("masuda", q.First().Value());
		}

		[TestMethod]
		public void TestNormal3()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XAttribute("id", "1"),
						new XElement("name", "masuda"),
						new XElement("age","44")),
					new XElement("person",
						new XAttribute("id", "2"),
						new XElement("name", "yamada"),
						new XElement("age","20")),
					new XElement("person",
						new XAttribute("id", "3"),
						new XElement("name", "tanaka"),
						new XElement("age","10"))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.Attrs("id") == "2")
				.FirstOrDefault();
			Assert.AreEqual("person", q.TagName());
			// 拡張メソッドを利用
			Assert.AreEqual("yamada", q.Child("name").Value());
			Assert.AreEqual("20", q.Child("age").Value());
		}
		
		[TestMethod]
		public void TestNormal4()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XAttribute("id", "1"),
						new XElement("name", "masuda"),
						new XElement("age","44")),
					new XElement("person",
						new XAttribute("id", "2"),
						new XElement("name", "yamada"),
						new XElement("age","20")),
					new XElement("person",
						new XAttribute("id", "3"),
						new XElement("name", "tanaka"),
						new XElement("age","10"))));


			// クエリ文にしてみる
			var q = from n in new XNavigator(doc.FirstNode)
					where n.Attrs("id") == "2"
					select n;

			Assert.AreEqual("person", q.First().TagName());
			// 拡張メソッドを利用
			Assert.AreEqual("yamada", q.First().Child("name").Value());
			Assert.AreEqual("20", q.First().Child("age").Value());
		}

	}

■拡張メソッドを追加する

で、普通にやると断念してしまうので…無理矢理、XNode に拡張メソッドを追加します。
本来は、TagName プロパティ、Value プロパティのように「プロパティ」にしたいのですが、プロパティの拡張メソッドはできないので TagName() メソッド、Value() メソッドにします。

	/// <summary>
	/// XNodeをXElement風に使う拡張クラス
	/// </summary>
	public static class XNodeExtentions
	{
		public static string TagName(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Name.ToString();
			}
			else
			{
				return "";
			}
		}
		public static string Value(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Value;
			}
			else
			{
				return "";
			}
		}
		public static string Attrs(this XNode n, string key)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				var attr = ((XElement)n).Attribute(key);
				if ( attr != null ) {
					return attr.Value;
				}
			}
			return "";
		}
		public static XNode Child(this XNode nd, string tag)
		{
			if (nd.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)nd).Nodes().Where(n => n.TagName() == tag).FirstOrDefault();
			}
			else
			{
				return null;
			}

		}
	
	}

すると、上記のテストコードが通るようになります。

	var q = new XNavigator(doc.FirstNode)
		.Where(n => n.TagName() == "name")
		.FirstOrDefault();
	Assert.AreEqual("masuda", q.Value());

ラムダ式の中の n が XNode なので、TagName() メソッドを使います、ってな具合。XElement の場合は Value プロパティになる訳で、ここのところ、

  • XNode.Value() メソッド
  • XElement.Value プロパティ

という「メソッド名とプロパティ名が同じ」という(多分)VB では使えないメソッドが作られていますが、まあ良しとしましょう(後で確認してみますか)。

こうすると、XDocument もツリー構造でサーチができるのでコードが簡単になりますね。って話で。
自前の XmlNode でもいいんだけど、node / “tag” のような書き方ができないから、どうしようかね、って具合です。

カテゴリー: C#, XmlDom | [C#] XNode を使って XNavigator を作る はコメントを受け付けていません

[c#]iTuens の api を使って App Store のアプリ情報の取得

iTunesより早い「高速AppStore検索」作りました。 – ku-sukeのはてなダイアリー
http://d.hatena.ne.jp/ku-suke/20100228
iPhoneアプリの値下げ情報をつぶやくBotを作りました。あと、App storeのxmlの取得の仕方。 ≪ KORESS: ジャパニーズ・モダン・ドンブラコ
http://koress.jp/2009/10/iphonebotapp_storexml.html

あたりを見て、iTunes の web api を叩けば ok ってことで、簡単に。

private void button1_Click(object sender, EventArgs e)
{

	// url を取得
	string id = "6015";
	string url = "http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/browse?path=%2F36%2F" + id + "%2F1";
	WebClientEx client = new WebClientEx();
	// cookie を設定
	client.Cookie = new CookieContainer();
	client.Headers.Add("User-Agent", "iTunes/10.6.3 (Windows; Microsoft Windows 7 Ultimate Edition Service Pack 1 (Build 7601)) AppleWebKit/534.57.2");
	client.Headers.Add("X-Apple-Store-Front", "143462-9");
	// client.Headers.Add("Accept-Encoding", "gzip");
	StreamReader sr = new StreamReader(client.OpenRead(url));
	string content = sr.ReadToEnd();
	sr.Close();

	Debug.Print("length:{0}", content.Length);

	// ファイルに出力
	StreamWriter sw = new StreamWriter("result.xml", false);
	sw.Write(content);
	sw.Close();
	textBox1.Text = string.Format("success: {0}", content.Length);
}

受信する時に gzip のほうが小さくなるのですが、zip 解凍が面倒なので付けていません。つけないと普通の text で取得できます。
ただし、カテゴリによって非常にでかい(ファイナンシャルだと10MBぐらい)なので、注意が必要です。

取得したデータは XML 形式で、objective-c を書いた方であれば、ああなるほどなプロパティリストな構造になっています。
なので、dict タグの下に、key-value という形式なのですが…これがプロパティリストのアクセス手段がない c# では面倒で orz

<dict>
  <key>artistId</key><integer>284946266</integer>
  <key>artistName</key><string>e-Agent</string>
  <key>buy-only</key><true/>
  <key>buyParams</key><string>productType=C&amp;salableAdamId=286058381&amp;pricingParameters=STDQ&amp;price=0&amp;ct-id=14</string>
  <key>genre</key><string>ファイナンス</string>
  <key>genreId</key><integer>6015</integer>
  <key>itemId</key><integer>286058381</integer>
  <key>itemName</key><string>3in1 Mortgage Calc</string>
  <key>kind</key><string>software</string>
  <key>playlistName</key><string>3in1 Mortgage Calc</string>
  <key>popularity</key><string>0.0</string>
  <key>price</key><integer>0</integer>
  <key>priceDisplay</key><string>無料</string>
  <key>rating</key>

自前の ExDoc を使ってこんなことをしています(ExDocは、そのうち、XmlDom, HtmlDom になる予定です)。

private void button2_Click(object sender, EventArgs e)
{
	var doc = new EXDocument();
	doc.Load("result.xml");

	var items = doc * "key" == "itemId";
	var lst = new List<App>();
	foreach ( var it in items )
	{
		var pa = it.Parent;
		lst.Add(
			new App
			{
				itemId = pa.ChildNodes[13].Value,
				itemName = pa.ChildNodes[15].Value
			});
	}
	dataGridView1.DataSource = lst;
}

key の値が「itemId」のタグを拾ってきて、親要素を取得。
その子ノードを ChildNodes でピンポイントで拾うという荒業です。まあ、これで用途は足りるかと。

実行結果はこんな感じ。

このデータをローカルファイルに保存しておけば、iTunes を使わなくても高速検索ができますよ、ってな具合ですね。
Web 上に乗せる場合はちょっと工夫が要りますが基本は同じ。一定時間で取得するようにすれば、最新アプリ情報とかも取れるかも。

カテゴリー: C# | [c#]iTuens の api を使って App Store のアプリ情報の取得 はコメントを受け付けていません

[C#] XElement とは違う LINQ できる XmlNode を作成する

昨日の続きで、XmlNode に navigator の機能を付けてみます。
xml に関するナビゲータは、既に XPathNavigator クラスがあって、XPATH の記法を使って XML のツリー構造を探索できるのですが…当たり前なのですが「XPATHを使わないといけない」という制約があります。

XPathNavigator クラス (System.Xml.XPath)
http://msdn.microsoft.com/ja-jp/library/system.xml.xpath.xpathnavigator(v=vs.110).aspx

まあ、xpath で細々と書ればいいんだけど、もっと大雑把に C# の文法に近く、というか LINQ 近い形で探索がしたいなぁと思った…というのは嘘で、成り行き上こうなりましたって感じです。HTML の探索方法を色々模索していったら、LINQ の Where メソッドを使う(IEnumable<> を使う)ほうが一番妥当かというパターンなのです。

■IEnumable<>部分を HtmlDoc からコピペ

以前作ったところから、そのままコピーして手直し。
IEnumerator<XmlNode> のところは、内部クラスにして XmlNavigator.Enumerator にします。

public class XmlNavigator : IEnumerable<XmlNode>
{
	XmlNode _root;

	public XmlNavigator(XmlNode root)
	{
		_root = root;
	}

	public IEnumerator<XmlNode> GetEnumerator()
	{
		return new Enumerator(_root);
	}

	System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
	{
		return new Enumerator(_root);
	}

	protected class Enumerator : IEnumerator<XmlNode>
	{
		XmlNode _root;
		XmlNode _cur;

		public Enumerator(XmlNode root)
		{
			_root = root;
			_cur = null;
		}


		public XmlNode Current
		{
			get { return _cur; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _cur; }
		}

		public bool MoveNext()
		{
			if (_cur == null)
			{
				_cur = _root;
				return true;
			}
			if (_cur.Children.Count > 0)
			{
				_cur = _cur.Children[0];
				return true;
			}
			if (_cur.NextSubling != null)
			{
				_cur = _cur.NextSubling;
				return true;
			}
			XmlNode cur = _cur;
			while (true)
			{
				XmlNode pa1 = cur.Parent;
				if (pa1 == null)
				{
					_cur = null;
					return false;
				}
				if (pa1.NextSubling != null)
				{
					_cur = pa1.NextSubling;
					return true;
				}
				cur = pa1;
			}
		}

		public void Reset()
		{
			_cur = null;
		}
	}
}

MoveNext メソッドが若干ややこしくて、XML のツリー構造を親子関係を伝って探索していきます。バイナリサーチの要領ですね。
次の兄弟要素を拾うことが必要なので、XmlNode.NextSubling メソッドを追加しています。

		public XmlNode NextSubling
		{
			get
			{
				if (this.Parent == null)
				{
					return null;
				}
#if true
				var it = this.Parent.Children.GetEnumerator();
				while ( it.MoveNext() ) {
					if ( it.Current == this ) {
						if ( it.MoveNext() ) {
							return it.Current;
						} else {
							return null;
						}
					}
				}
				return null;
#else
				for (int i = 0; i < this.Parent.Children.Count; i++)
				{
					XmlNode nd = this.Parent.Children[i];
					if (this.Equals(nd))
					{
						if (i < this.Parent.Children.Count - 1)
						{
							return this.Parent.Children[i + 1];
						}
						else
						{
							return null;
						}
					}
				}
				return null;
#endif
			}
		}

こんな感じでいちいち探索しているのでスピード的には遅いのですが、用途は足りるのでOKとします。
最初は for 文で書いておいて、後から GetEnumerator で書き直してありますが…多分スピードは変わらないでしょう。

■テストコードを書く

例によって、テストコードを書きます。昨日書いた XmlNode の構築メソッドチェーンを使って、ぽちぽちと移植。

	[TestClass]
	public class TestXmlNavi
	{
		[TestMethod]
		public void TestNormal()
		{
			var root = new XmlNode("root")
				.Node("person")
				.Node("name", "masuda")
				.Root;

			// タグが検索できた場合
			var q = new XmlNavigator(root)
				.Where(n => n.TagName == "name")
				.FirstOrDefault();
			Assert.AreEqual("masuda", q.Value);

			// タグが見つからなかった場合
			q = new XmlNavigator(root)
				.Where(n => n.TagName == "error")
				.FirstOrDefault();
			Assert.AreEqual(null, q);

		}

		[TestMethod]
		public void TestNormal2()
		{
			var root = new XmlNode("root")
				.Node("person")
				.AddNode("name", "masuda")
				.AddNode("name", "yamada")
				.AddNode("name", "yamasaki")
				.Root;

			// タグが検索できた場合
			var q = new XmlNavigator(root)
				.Where(n => n.TagName == "name")
				;
			Assert.AreEqual(3, q.Count());
			Assert.AreEqual("masuda", q.First().Value);
		}

		[TestMethod]
		public void TestNormal3()
		{
			var root = new XmlNode("root")
				.Node("person").AddAttr("id", "1")
					.AddNode("name", "masuda")
					.AddNode("age", "44")
				.Parent
				.Node("person").AddAttr("id", "2")
					.AddNode("name", "yamada")
					.AddNode("age", "20")
				.Parent
				.Node("person").AddAttr("id", "3")
					.AddNode("name", "tanaka")
					.AddNode("age", "10")
				.Root;

			// タグが検索できた場合
			var q = new XmlNavigator(root)
				.Where(n => n.Attrs["id"] == "2")
				.FirstOrDefault();
			Assert.AreEqual("person", q.TagName);
			// ExDoc記述
			Assert.AreEqual("yamada", q / "name");
			Assert.AreEqual("20", q / "age");
		}

		[TestMethod]
		public void TestNormal4()
		{
			var root = new XmlNode("root")
				.Node("person").AddAttr("id", "1")
					.AddNode("name", "masuda")
					.AddNode("age", "44")
				.Parent
				.Node("person").AddAttr("id", "2")
					.AddNode("name", "yamada")
					.AddNode("age", "20")
				.Parent
				.Node("person").AddAttr("id", "3")
					.AddNode("name", "tanaka")
					.AddNode("age", "10")
				.Root;

			// クエリ文にしてみる
			var q = from n in new XmlNavigator(root)
					where n.Attrs["id"] == "2"
					select n;

			Assert.AreEqual("person", q.First());
			// ExDoc記述
			Assert.AreEqual("yamada", q.First() / "name");
			Assert.AreEqual("20", q.First() / "age");
		}

		[TestMethod]
		public void TestNormal5()
		{
			var root = new XmlNode("root")
				.Node("person").AddAttr("id", "1")
					.AddNode("name", "masuda")
					.AddNode("age", "44")
				.Parent
				.Node("person").AddAttr("id", "2")
					.AddNode("name", "yamada")
					.AddNode("age", "20")
				.Parent
				.Node("person").AddAttr("id", "3")
					.AddNode("name", "tanaka")
					.AddNode("age", "10")
				.Root;

			// クエリ文にしてみる
			var nd = from n in new XmlNavigator(root)
					 where n % "id" == "2"
					 select n;
			XmlNode xn = nd.FirstOrDefault();

			// 本来は、以下のようにしたのだが暗黙キャストが作れない
			/*
			XmlNode xn = from n in new XmlNavigator(root)
						 where n % "id" == "2"
						 select n;
			*/

			Assert.AreEqual("person", xn);
			// ExDoc記述
			Assert.AreEqual("yamada", xn / "name");
			Assert.AreEqual("20", xn / "age");
		}
	}

TestNormal3 あたりからある Assert.AreEqual(“yamada”, q / “name”); は、子要素から「name」タグを取得するという ExDoc の文法です…つーか、operator の多重定義。
XmlNode クラスに以下の operator を追加しておきます。

	#region sweet operator 
	public static XmlNode operator / ( XmlNode node, string tag ) 
	{
		var nd = node.Children.Find( n => n.TagName == tag ) ;
		return nd ?? XmlNode.Empty ;
	}
	public static implicit operator string(XmlNode node)
	{
		return node.Value;
	}
	public static string operator %(XmlNode node, string key)
	{
		return node.Attrs[key];
	}

ExDoc を作ったときは等価オペレータ(==演算子)も多重定義していましたが、今回は LINQ に任せるので、/演算子と%演算子だけを多重定義します。
TestNormal5 メソッドの暗黙のキャストはうまく定義できないので、断念してます。何か逃れることはできないかと、後でチェック。

■できあがったコード

XmlNavigator クラスは最初に示した通りです。XmlNode クラスは多少手を加えてテストはOKということで。
IEnumerator<XmlNode>.MoveNext さえきっちりと実装していけば、LINQ 文法で動くのでかなりコーディングが楽です。

■IEnumerable<XmlNode> からの暗黙のキャストが作れない

ExDoc で作ったように、LINQ の結果を XmlNode で直接受けたい(暗黙のキャストを受けたい)のですが、これが実装できません。

	XmlNode xn = from n in new XmlNavigator(root)
				 where n % "id" == "2"
				 select n;

下記のように作るのですが、

class XmlNode {
...
	public static implicit operator XmlNode(IEnumerable<XmlNode> lst)
	{
		return lst.FirstOrDefault() ?? XmlNode.Empty;
	}
}

コンパイルするとこんなエラーが。

エラー	1	'Moonmile.XmlDom.XmlNode.implicit operator Moonmile.XmlDom.XmlNode(System.Collections.Generic.IEnumerable<Moonmile.XmlDom.XmlNode>)': インターフェイスとの間におけるユーザー定義の変換は許可されていません。

ってな具合で、インターフェースからキャストはできないそうです。うーむ残念。
なんかいい方法はないですかねぇ。多分、ToList メソッドのように拡張メソッドをつければいいんでしょうが

	XmlNode xn = (from n in new XmlNavigator(root)
				 where n % "id" == "2"
				 select n ).ToXmlNode();

のように折角のクエリ文法に括弧を入れないと駄目なのでいまいち過ぎ。
まあ、メソッド的に

	var xn = new XmlNavigator(root)
		.Where(n => n % "id" == "2")
		.FirstOrDefault();

と書くと、ひとつの XmlNode が取れるんので十分なんですけどね。後で考えますか。

カテゴリー: C# | [C#] XElement とは違う LINQ できる XmlNode を作成する はコメントを受け付けていません

[C#] XElement 風に XML を構築するクラスを作ってみる

困ったときにそのまま使える LINQ to XML コードサンプル 18 パターン [1/2] – Web/DB プログラミング徹底解説
http://keicode.com/dotnet/linq-to-xml-101.php
XElement クラス (System.Xml.Linq)
http://msdn.microsoft.com/ja-jp/library/system.xml.linq.xelement.aspx

を参考にしながら、XML をメソッドチェーンを使って構築するクラスを作ってみます。まぁ、HtmlDom を作るときの副産物…と言うか、練習問題的に。

■使い方サンプルを作る

TDD と UIDD のハイブリッドプロセス(という命名をしておく)、ということで使い方のサンプルを先に作ります。
これを先に作っておくと、ある程度のマニュアルがなくても使える「感覚的に正しい」プログラミングコードができあがります。D.A.ノーマンの言う「ユーザーインターフェース」ってやつです。

public void test1()
{
	var root = new XmlNode("root")
		.Node("person")
		.Node("name", "masuda")
		.Root;

	var root2 = new XmlNode("root")
		.Node("person")
		.AddNode("name", "masuda")
		.AddNode("age", "44")
		.AddNode("address", "itabashi-ku")
		.Root;

	var root3 = new XmlNode("root")
		.Node("person")
		.AddAttr("name", "masuda")
		.AddAttr("age", "44")
		.SetValue("masuda tomoaki")
		.Root;


	var root4 = new XmlNode("persons")
		.Node("person").AddAttr("id", "1")
			.AddNode("name", "masuda tomoaki")
			.AddNode("age", "44")
		.Parent
		.Node("person").AddAttr("id", "2")
			.AddNode("name", "yamada taro")
			.AddNode("age", "20")
		.Root;
}

実は、.Parent と .Root は後付、SetValue はやむなくって感じですね。
最初に作ったのはこんな感じ。

	var root = new XmlNode("root")
		.Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki");

「動詞」を使わずに「名詞」だけで、繋げてみたかったのですが、Value プロパティと、Value メソッドが曖昧になってしまうのでパス。
あと、子ノードを連続追加する時に Node だけだと駄目で、Node.Parent.Node(“tag”) と変なことをしないといけないので、AddNode に変更。それに伴って、Attr も AddAttr に変更しています。

それと、最後に Root プロパティを入れたのは、テストコードを書いたときに、

	var root = new XmlNode("root");
	root.Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki");
    Assert.AreEqual("...", root.Xml );

のように「わざわざ」ルート要素を保存しないといけないという面倒があるので、ルートに戻すプロパティを用意します。

	var root = new XmlNode("root")
	    .Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki")
        .Root ;
    Assert.AreEqual("...", root.Xml );

こっちのほうがすっきりするし、後から意味が分かり易いのです。

■インターフェースに従って、コンパイルが通るようにコーディング

テストコードを先に書いてもいいのですが、Visual Studio のインテリセンスを利用したいので、コンパイルが通るまでのコードを書きます。
素のエディタで書くときは、逆にテストコードから書いたりします。と言うのも、素のエディタの場合は、テストコードのコンパイルエラーからメソッド/プロパティを追加することになるので、テストコードが先のほうがやり易いのです。
この違いは(私にとって)重要です。

■テストコードを書く

下記では、一気に追加していますが、実際にはひとつひとつ追加しています。
簡単なテストから複雑なテストへというのはテストの定番です。
実は Xml プロパティは、テストコードを書くときに追加しています。こうするとテストがやりやすいですからね。

	[TestMethod]
	public void TestNormal1()
	{
		var root = new XmlNode("root")
			.Node("person")
			.Node("name", "masuda")
			.Root;

		Assert.AreEqual("<root><person><name>masuda</name></person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal2()
	{
		var root = new XmlNode("root")
			.Node("person")
			.AddNode("name", "masuda")
			.AddNode("age", "44")
			.AddNode("address", "itabashi-ku")
			.Root;

		Assert.AreEqual("<root><person><name>masuda</name><age>44</age><address>itabashi-ku</address></person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal3()
	{
		var root = new XmlNode("root")
			.Node("person")
			.AddAttr("name", "masuda")
			.AddAttr("age", "44")
			.SetValue("masuda tomoaki")
			.Root;

		Assert.AreEqual("<root><person name=\"masuda\" age=\"44\">masuda tomoaki</person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal4()
	{
		var root = new XmlNode("persons")
			.Node("person").AddAttr("id", "1")
				.AddNode("name", "masuda tomoaki")
				.AddNode("age", "44")
			.Parent
			.Node("person").AddAttr("id", "2")
				.AddNode("name", "yamada taro")
				.AddNode("age", "20")
			.Root;
			;

		Assert.AreEqual("<person>" + 
			"<person id=\"1\"><name>masuda tomoaki</name><age>44</age></person>" +
			"<person id=\"2\"><name>tamada taro</name><age>20</age></person>" +
			"</persons>",
			root.Xml);
	}

■コードが完成品

でもって、テストを通るようにしたのが以下、150行程度のコードです。
Xml プロパティは、HtmlDom からコピペしているのでちょっと無駄なコードが入っていますが、まぁこの程度で書けるのだから C# って便利やなぁ、と。
これを HtmlDom に組み入れるかどうか思案中。

/// <summary>
/// XML node class
/// </summary>
public class XmlNode
{
	#region property
	public string TagName { get; set; }
	public string Value { get; set; }
	public XmlNodeCollection Children { get; set; }
	public XmlAttrCollection Attrs { get; set; }
	public XmlNode Parent { get; set; }
	#endregion

	#region constractor 
	public XmlNode(string tag, string value = "")
	{
		this.TagName = tag;
		this.Value = value;
		this.Children = new XmlNodeCollection();
		this.Children.Parent = this;
		this.Attrs = new XmlAttrCollection();
	}
	#endregion

	#region creator for method chain
	public XmlNode Node(string tag, string value = "")
	{
		var nd = new XmlNode(tag, value);
		this.Children.Add(nd);
		return nd;
	}
	public XmlNode AddNode(string tag, string value = "")
	{
		this.Node(tag, value);
		return this;
	}

	public XmlNode AddAttr(string key, string value)
	{
		this.Attrs.Add(new XmlAttr(key, value));
		return this;
	}
	public XmlNode SetValue(string value)
	{
		this.Value = value;
		return this;
	}
	#endregion

	#region xml string serialize
	public string Xml
	{
		get
		{
                string s = "";
                if (this.TagName == "" || this.TagName == "#text" || this.TagName == "#comment")
                {
                    s = this.Value.Trim();
                }
                else
                {
                    s += string.Format("<{0}", this.TagName);
                    foreach (var at in this.Attrs)
                    {
                        s += string.Format(" {0}=\"{1}\"", at.Key, at.Value);
                    }
                    if (this.Children.Count > 0)
                    {
                        s += ">";
					s += this.Value;
                        foreach (var el in this.Children)
                        {
                            s += el.Xml;
                        }
                        s += string.Format("</{0}>", this.TagName);
                    }
				else if (this.Value != "")
				{
					s += ">";
					s += this.Value;
					s += string.Format("</{0}>", this.TagName);
				} 
				else 
                    {
                        s += "/>";
                    }
                }
                return s;
		}
	}
	#endregion

	public XmlNode Root
	{
		get
		{
			XmlNode nd = this;
			while (nd.Parent != null)
			{
				nd = nd.Parent;
			}
			return nd;
		}
	}
}

/// <summary>
/// XML node collection
/// </summary>
public class XmlNodeCollection : List<XmlNode> {
	public XmlNode Parent { get; set; }
	public new XmlNode Add(XmlNode nd) 
	{
		base.Add(nd);
		nd.Parent = this.Parent;
		return nd;
	}

}

/// <summary>
/// XML attribute class
/// </summary>
public class XmlAttr
{
	public string Key { get; set; }
	public string Value { get; set; }

	public XmlAttr(string key, string value)
	{
		this.Key = key;
		this.Value = value;
	}
}
/// <summary>
/// XML Attribute collection
/// </summary>
public class XmlAttrCollection : List<XmlAttr> { }

カテゴリー: C# | [C#] XElement 風に XML を構築するクラスを作ってみる はコメントを受け付けていません

[C#] IEnumerable<> と IEnumerator<> を使って独自のリストを作ってみる

HtmlDom のイテレーターを作る前に、List<> で利用される LINQ がどう動くのかをチェック。
手始めには、IEnumerable と IEnumerator を使えばよいとのことなので、自前で作ってみます。

IEnumerable(T) インターフェイス (System.Collections.Generic)
http://msdn.microsoft.com/ja-jp/library/9eekhta0.aspx
IEnumerable.GetEnumerator メソッド (System.Collections)
http://msdn.microsoft.com/ja-jp/library/system.collections.ienumerable.getenumerator.aspx

MSDN 自体に既にサンプルコードが載っているのでそれを使っていきます。

■ランダムな値を返す RandomNum リストを作る

ランダムな値を返すならば、Random を使えばいいだけなのですが、まぁ、これを LINQ で使えるようにします。

foreach ( var i in rnd ) {
  Debug.Print( i );
}

なんてことをやると無限に繰り返してしまうようなモノですね(苦笑)。

	class RandomNum : IEnumerable<int>
	{
		int _max ;
		public RandomNum(int max)
		{
			_max = max;
		}

		public IEnumerator<int> GetEnumerator()
		{
			return new RandomNumEnum(_max);
		}

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return new RandomNumEnum(_max);
		}
	}
	class RandomNumEnum : IEnumerator<int>
	{
		int _max;
		Random _rnd;
		int _cur;
		public RandomNumEnum(int max)
		{
			_max = max;
			_rnd = new Random();
		}
		public int Current
		{
			get { return _cur; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _cur; }
		}

		public bool MoveNext()
		{
			_cur = _rnd.Next(_max);
			return true;
		}

		public void Reset()
		{
			_rnd = new Random();
		}
	}

RandomNum が、List<int> にあたるコレクションで、RandomNumEnum がイテレーターです。ちょうど C++ の list<int> と list<int>::iterator の関係…でいいのかな? IEnumerable, IEnumerator で宣言されているメソッドは、右クリック「インターフェース実装」→「インターフェースの実装」を選択すると Visual Studio がひな形を作ってくれます。

これを利用する場合は、

private void button1_Click(object sender, EventArgs e)
{
	// 初期化
	RandomNum rnd = new RandomNum(100);
	// 最初の50件を取得
	listBox1.DataSource = rnd.Take(50).ToList();
}

private void button2_Click(object sender, EventArgs e)
{
	// 初期化
	RandomNum rnd = new RandomNum(100);
	// 10から20までの値を取得
	listBox1.DataSource = rnd.Where(x => 10 <= x && x <= 20)
		.Take(30).ToList();
}

上記のように、Take メソッドを使って最初の50件を取得します。foreach を使うと無限に取得できるので暴走するハズです。
また、where メソッドを使って条件を付けることもできます。あまり意味がありませんが。

実行するとこんな感じ。

普通にランダムに表示したのと変わりませんが、一気にランダムな値を作る時に便利かと(便利なのか?)

■サイクリックなコレクションを作る

なんとなくコレクションの使い方が分かったので、今度はサイクリックな配列を作ってみます。最後の要素の次が先頭になってぐるぐるとまわるだけのリストです。

ランダム値と同じように、IEnumerable<string> を実装するリストと、IEnumerator<string> を実装するイテレーターを作ります。

	class CyclicArray : IEnumerable<string>
	{
		List<string> _lst;
		public CyclicArray(List<string> lst)
		{
			_lst = lst;
		}

		public IEnumerator<string> GetEnumerator()
		{
			return new CyclicArrayEnum(_lst);
		}

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return new CyclicArrayEnum(_lst);
		}

	}
	class CyclicArrayEnum : IEnumerator<string>
	{
		List<string> _lst;
		int _pos;
		public CyclicArrayEnum(List<string> lst)
		{
			_lst = lst;
			_pos = -1;
		}

		public string Current
		{
			get { return _lst[_pos] ; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _lst[_pos]; }
		}

		public bool MoveNext()
		{
			_pos++;
			if (_pos >= _lst.Count)
				_pos = 0;
			return true;
			throw new NotImplementedException();
		}

		public void Reset()
		{
			_pos = -1;
		}
	}

サイクリックなので、CyclicArrayEnum::MoveNext メソッドは必ず true を返します。これも foreach をすると無限大のパターンになるのですが、まあ良しとします。内部的には _pos で現在位置を保存しておいて、末尾に来ると先頭に戻すという処理を入れます。

private void button3_Click(object sender, EventArgs e)
{
	var lst = new List<string>() {
		"1:masuda",
		"2:tomoaki",
		"3:yamasaki",
		"4:yumi",
		"5:kaho",
	};
	// 初期化
	var ca = new CyclicArray(lst);
	// 13件取得する
	listBox1.DataSource =
		ca.Take(13).ToList();
}

 

使う場合は、こんな風に List<string> を渡して、Take メソッドで一定数を取るというスタイルですね。
配列としては5つの要素しかないのですが、take で 13個を要求するとサイクリックに返してくれます。
簡単ですが、これでコレクションとイテレーターは作れそうなのが分かりました、と。

■ツリー構造を子孫まで巡って探すイテレーターはどのように作るのか?

普通のコレクションの場合は一段階しか要素を探しに行かないのですが、XML 構造のようにツリー構造になっていると再帰的に子要素を探す必要があるのです。

なので、これを擬似的に書いてみると…なところで詰まってしまったので後日。

カテゴリー: C# | [C#] IEnumerable<> と IEnumerator<> を使って独自のリストを作ってみる はコメントを受け付けていません