銀の匙が載りますか!戻ってきてくれてよかった。
BIRDMENも掲載される号だし。楽しみだー。
2015年4月16日木曜日
2015年1月14日水曜日
Import時の重複チェックにLINQ(GroupBy)を利用する
外部のシステムからデータをインポートする機能は、多くのシステムで必要になってきます。CSVなりXMLなりの標準化された形式で送られたデータを、一括でシステムに取り込む機能です。
この場合、普通の入力とは異なり、一括で処理するゆえのメンドクササがありますよね。
たとえば、ユーザ情報を外部システムからインポートする場合。想定するケースによりますが、ユーザIDがインポートデータ中に重複して存在する可能性があったりすると、せめて取り込みの事前のチェックを行っておきたいケースもあると思います。が、これが結構面倒。
これをGroupByメソッドを活用して、なるべくシンプルに記述してみます。想定する前提はこんな感じだとします。
この場合、普通の入力とは異なり、一括で処理するゆえのメンドクササがありますよね。
たとえば、ユーザ情報を外部システムからインポートする場合。想定するケースによりますが、ユーザIDがインポートデータ中に重複して存在する可能性があったりすると、せめて取り込みの事前のチェックを行っておきたいケースもあると思います。が、これが結構面倒。
これをGroupByメソッドを活用して、なるべくシンプルに記述してみます。想定する前提はこんな感じだとします。
- インポートするユーザ情報データは、すでにオブジェクトへのマッピングがされており、コレクション化されている前提。
- ユーザ情報データは「UserInfo」クラスに格納されており、ユーザIDは文字列のメンバUserIdとして存在する。
- ユーザ情報データのコレクションを入力として、ユーザIDの重複チェックを行う静的メソッド「ValidateNotDuplicated」を作成する。
- このメソッドは、ユーザIDの重複があった場合、コンソールにエラーメッセージを出力し、メソッドの戻り値はfalseを返す。重複がなければそのままtrueを返す。
- エラーメッセージには、重複しているユーザIDを表示する。複数のユーザIDが重複していた場合は、そのすべてを表示する。
このような処理を書くと、こんな感じになりますね。
public static bool ValidateNotDuplicated(IEnumerable<UserInfo> users)
{
var dupusers = users.GroupBy(u => u.UserId)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToArray();
if (dupusers.Length > 0)
{
Console.WriteLine("User:{0} was duplicated.", string.Join(",", dupusers);
return false;
}
return true;
}
つまり、「コレクションをUserIdでグループ化し(GroupBy)」、「そのうち2つ以上存在するものを選び出し(Where)」、「キーとなっているUserIdのみを対象として(Select)」、「配列に変換する(ToArray)」。という処理を行っています。
ちょっとだけバリエーションを考えて見ましょうか。たとえば、
ちょっとだけバリエーションを考えて見ましょうか。たとえば、
- 「UserInfo」クラスには、データが更新された時間を示す「Updated」メンバ(DateTime型)も持っている。
- このユーザ情報データのコレクションを入力として、ユーザIDが重複していた場合、「Updated」が最新のもの以外を取り除いたコレクションを返す、静的メソッド「SelectLatest」を作成する。
この場合ならもっとシンプルで、
public static IEnumerable<UserInfo> SelectLatest(IEnumerable<UserInfo> users)
{
return users.GroupBy(u => u.UserId)
.Select(g => g.OrderByDescending(u => u.Updated).First());
}
これだけになりますね。
ラベル:
C#
2015年1月12日月曜日
Enumerable.GroupByメソッドの威力
2015年も明けて間もなく2週間が経とうとしています。
おそくなりましたが、今年もよろしくお願いします。
今年の目標は「去年のエントリ数を超える」こと。2014年は5本だったようなので、達成できそうな目標にしてみました。
一発目のエントリは、「GroupBy」をテーマにしてみます。非常に強力なLINQのメソッドですが、なかなか使う機会が少ない人も多いのではないかと思います。
「GroupBy」メソッドは、SQLを触ったことのある人なら「group by」句をイメージしてもらうとほぼ間違いなしで、つまりは「コレクション内の各要素を、ある条件でグループ分けする」メソッドです。ぱっと見面倒そうですが、使い方はいたってシンプルです。
サンプルを書いてみましょう。
おそくなりましたが、今年もよろしくお願いします。
今年の目標は「去年のエントリ数を超える」こと。2014年は5本だったようなので、達成できそうな目標にしてみました。
一発目のエントリは、「GroupBy」をテーマにしてみます。非常に強力なLINQのメソッドですが、なかなか使う機会が少ない人も多いのではないかと思います。
「GroupBy」メソッドは、SQLを触ったことのある人なら「group by」句をイメージしてもらうとほぼ間違いなしで、つまりは「コレクション内の各要素を、ある条件でグループ分けする」メソッドです。ぱっと見面倒そうですが、使い方はいたってシンプルです。
サンプルを書いてみましょう。
- データメンバとして、日付(Date)と売上額(Amount)を持つSalesクラスがあります。
- Salesクラスは、ある指定期間の日別の売上額を列挙するEnumerate静的メソッドを持ちます。
- ここで、上記Salesクラスを使い、ある年(ここでは2014年)の曜日別の売り上げの総合計、平均値、中間値を算出します。
この処理は以下のコードになります。
static void ShowGroupedAmounts()
{
var from = new DateTime(2014, 1, 1);
var to = new DateTime(2014, 12, 31);
var group = Sales.Enumerate(from, to)
.GroupBy(s => s.Date.DayOfWeek)
.OrderBy(g => g.Key);
Console.WriteLine("DayOfWeak,Total Amount,Average,Median");
foreach (var g in group)
{
var cnt = g.Count();
Console.WriteLine(
"{0},{1},{2},{3}",
g.Key,
g.Sum(s => s.Amount),
Math.Round(g.Average(s => s.Amount), 1),
g.OrderBy(s => s.Amount).Skip(cnt / 2).First().Amount);
}
}
非常にシンプルなコードで、キー(ここでは曜日)ごとにグループ化できていると思います。なお、GroupByメソッドの戻り値は以下の形態になります。
IEnumerable<IGrouping<DayOfWeak, Sales>>
IGroupingは、IEnumerable<T>にキー情報を加えたもの。と考えればOKです。
ラベル:
C#
2014年12月21日日曜日
2014年10月27日月曜日
KeyedCollectionクラス
.netでコレクションというと、ほとんどのケースでListとDictionaryがあれば事足りてしまう印象があります。
とはいえ、標準のクラスライブラリには、かなりたくさんのバリエーションがあり、多くはほとんど使われていないんじゃないかと思います。(System.Collection名前空間に属するものは使わないほうがよいですが。)
そんな中で、System.Collection.ObjectModel名前空間に属するものに、たまに使いたくなるものがあったりします。
そのうちの一つに、「KeyedCollection」クラスがあります。これは、ListとDictionaryの両方の特徴を持つようなクラスになっていて、
と書かせるより、
のほうがタイプ量も少ないし、Intellisenseもある程度効いてくれます。そして、SelectableKeyedCollectionクラスをintenalとすることで、使う人はその存在を知る必要がなくなり、KeyedCollection<TKey, TItem>を使うために、KeyedCollectionクラスさえ知っていれば良くなります。そのほうが親切だと思うのです。
.net FrameworkのTupleが同じような考え方になっています。
このクラスを使ったサンプルを書いてみます。ここでは、「あす以降の一週間のDateTimeをコレクションに突っ込み、曜日でのアクセスとシーケンシャルなアクセスを行う。」コンソールアプリケーションのサンプルです。なんかもう少し意味のあるサンプルにしたいところですが、それをやると簡潔に書けないので。
「System.Collection.ObjectModel」名前空間と、先のSelectableKeyedCollectionとKeyedCollectionが属する名前空間をusingした上で、
これをコンパイルして実行すると、以下の結果となります。(ちなみに今日は2014年10月27日)
個人的には、これなら使う気になります。
もし本当に汎用的なクラスに仕上げるなら、KeyedCollection<TKey, TItem>クラスには、コンストラクタがもう2つほどあるので、それらに対する備えをしておけば使い勝手の良いクラスができると思います。
とはいえ、標準のクラスライブラリには、かなりたくさんのバリエーションがあり、多くはほとんど使われていないんじゃないかと思います。(System.Collection名前空間に属するものは使わないほうがよいですが。)
そんな中で、System.Collection.ObjectModel名前空間に属するものに、たまに使いたくなるものがあったりします。
そのうちの一つに、「KeyedCollection」クラスがあります。これは、ListとDictionaryの両方の特徴を持つようなクラスになっていて、
- Dictionaryのようにキーで要素にアクセスできる。
- Listのように追加/挿入時の順番が保持され、Indexでアクセスできる。
- 要素の一部がキーとして扱われる。
- キーの重複は不可。
といった特徴を有します。なので、たとえばEntityの中にIDのような一意な値を持っていて、そのキーでアクセスするようなケースで有用です。さらに順番を保持させたいなら積極的に使うべきクラスかも。
ただ、大変惜しむらくは、KeyedCollectionクラスは抽象仮想クラスになっていて、中に収めるEntityごとにクラスを派生させて使う前提になってしまっています。因みに、派生クラスでオーバーロードするメソッドは基本的に一つのみで、Entityからキーを選びだすセレクタの「GetKeyForItem」メソッド。
使おうとするごとにクラスを一つ作らなければならず、Entityが異なればそれぞれに派生クラスを作らなければならない。今どきこれは大変めんどくさい。
なので、一回汎用的なクラスを作ったら、それを使いまわせるようにしておきたいですね。
戦略としては、「セレクタメソッドをFuncで渡すコンストラクタ」を用意し、先の「GetKeyForItem」メソッドではコンストラクタで渡されたFuncを使う。ことにすれば使いやすくなりそうです。
で、書いてみたのがこれ。
internal class SelectableKeyedCollection<TKey, TItem>
: KeyedCollection<TKey, TItem>
{
private Func<TItem, TKey> keySelector;
internal SelectableKeyedCollection(Func<TItem, TKey> keySelector)
{
this.keySelector = keySelector;
}
protected override TKey GetKeyForItem(TItem item)
{
return keySelector(item);
}
}
public static class KeyedCollection
{
public static KeyedCollection<TKey, TItem>
Create<TKey, TItem>(Func<TItem, TKey> keySelector)
{
return new SelectableKeyedCollection<TKey, TItem>(keySelector);
}
}
結局2つのクラスになっていますが、下のKeyedCollectionクラスは、上のSelectableKeyedCollectionを作るだけのクラスです。そしてSelectableKeyedCollectionクラスはinternal指定としています。
なぜにこんな2段階の作りになっているかというと、
なぜにこんな2段階の作りになっているかというと、
var collection = new SelectableKeyedCollection<X, Y>(x => x.y);
と書かせるより、
var collection = KeyedCollection.Create((X x) => x.y);
のほうがタイプ量も少ないし、Intellisenseもある程度効いてくれます。そして、SelectableKeyedCollectionクラスをintenalとすることで、使う人はその存在を知る必要がなくなり、KeyedCollection<TKey, TItem>を使うために、KeyedCollectionクラスさえ知っていれば良くなります。そのほうが親切だと思うのです。
.net FrameworkのTupleが同じような考え方になっています。
このクラスを使ったサンプルを書いてみます。ここでは、「あす以降の一週間のDateTimeをコレクションに突っ込み、曜日でのアクセスとシーケンシャルなアクセスを行う。」コンソールアプリケーションのサンプルです。なんかもう少し意味のあるサンプルにしたいところですが、それをやると簡潔に書けないので。
「System.Collection.ObjectModel」名前空間と、先のSelectableKeyedCollectionとKeyedCollectionが属する名前空間をusingした上で、
static void Main(string[] args)
{
var week = KeyedCollection.Create((DateTime dt) => dt.DayOfWeek);
foreach (var n in Enumerable.Range(1, 7))
{
week.Add(DateTime.Today.AddDays(n));
}
Console.WriteLine("Today is {0:yyyy/MM/dd (ddd)}", DateTime.Today);
Console.WriteLine();
Console.WriteLine("Next 7 days...");
foreach (var d in week)
{
Console.WriteLine(d.ToShortDateString());
}
Console.WriteLine();
Console.WriteLine(
"Next Friday is {0:yyyy/MM/dd (ddd)}", week[DayOfWeek.Friday]);
}
これをコンパイルして実行すると、以下の結果となります。(ちなみに今日は2014年10月27日)
Today is 2014/10/27 (月) Next 7 days... 2014/10/28 2014/10/29 2014/10/30 2014/10/31 2014/11/01 2014/11/02 2014/11/03 Next Friday is 2014/10/31 (金)
個人的には、これなら使う気になります。
もし本当に汎用的なクラスに仕上げるなら、KeyedCollection<TKey, TItem>クラスには、コンストラクタがもう2つほどあるので、それらに対する備えをしておけば使い勝手の良いクラスができると思います。
ラベル:
C#
2014年9月11日木曜日
文字列からファイル名に使えない文字を(取り除く|置き換える)
LINQの小ネタをひとつ。
ファイルを作るときのファイル名に、何らかの理由でファイル名として使えない文字が含まれていないかをチェックして、含まれているときには取り除いたり、置き換えたりして処理を続行する。というケースがままあります。
この手の細かい処理が結構見落としがちで、面倒だったりします。
これをLINQを使ってお気楽実装してみます。まぁ、見落としを防ぐことはできませんけどね。
結局のところ、入力された文字列を一文字ずつ見ていって、ファイル名として使えない文字なら取り除いたり、置き換えたりする処理を実装します。「一文字ずつ」見ていくので、LINQの出番ですね。
置き換える(ここではアンダースコア'_')なら、こうですかね。
で、取り除くならこう。
お気楽でしょ。
ファイルを作るときのファイル名に、何らかの理由でファイル名として使えない文字が含まれていないかをチェックして、含まれているときには取り除いたり、置き換えたりして処理を続行する。というケースがままあります。
この手の細かい処理が結構見落としがちで、面倒だったりします。
これをLINQを使ってお気楽実装してみます。まぁ、見落としを防ぐことはできませんけどね。
結局のところ、入力された文字列を一文字ずつ見ていって、ファイル名として使えない文字なら取り除いたり、置き換えたりする処理を実装します。「一文字ずつ」見ていくので、LINQの出番ですね。
置き換える(ここではアンダースコア'_')なら、こうですかね。
var invalidChars = Path.GetInvalidFileNameChars(); var converted = string.Concat( original.Select(c => invalidChars.Contains(c) ? '_' : c));
で、取り除くならこう。
var invalidChars = Path.GetInvalidFileNameChars(); var removed = string.Concat(original.Where(c => !invalidChars.Contains(c)));
お気楽でしょ。
ラベル:
C#
せめて「細々と」
ご無沙汰しておりました。。。
えー、最後に(まともに内容のある)エントリを立てたのが去年の8月だったので、すでに1年以上前のことだったりします。ひえー。
なんやかや忙しかったりしたのが、少し落ち着いてきたので、せめて「細々と続いてる」状態を目指して行きたいと思っております。
C#なネタは枯渇気味なので、どのくらい続くかわからんですが。
えー、最後に(まともに内容のある)エントリを立てたのが去年の8月だったので、すでに1年以上前のことだったりします。ひえー。
なんやかや忙しかったりしたのが、少し落ち着いてきたので、せめて「細々と続いてる」状態を目指して行きたいと思っております。
C#なネタは枯渇気味なので、どのくらい続くかわからんですが。
登録:
投稿 (Atom)