2014年10月27日月曜日

KeyedCollectionクラス

.netでコレクションというと、ほとんどのケースでListとDictionaryがあれば事足りてしまう印象があります。

とはいえ、標準のクラスライブラリには、かなりたくさんのバリエーションがあり、多くはほとんど使われていないんじゃないかと思います。(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段階の作りになっているかというと、

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つほどあるので、それらに対する備えをしておけば使い勝手の良いクラスができると思います。

2014年9月11日木曜日

文字列からファイル名に使えない文字を(取り除く|置き換える)

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)));


お気楽でしょ。

せめて「細々と」

ご無沙汰しておりました。。。

えー、最後に(まともに内容のある)エントリを立てたのが去年の8月だったので、すでに1年以上前のことだったりします。ひえー。

なんやかや忙しかったりしたのが、少し落ち着いてきたので、せめて「細々と続いてる」状態を目指して行きたいと思っております。

C#なネタは枯渇気味なので、どのくらい続くかわからんですが。

2014年1月7日火曜日

あ、あけましておめでとうござ。。。

うわぁ。気がつけば年が明けてしまいました。4ヵ月以上放置して…。

あ、あけましておめでとうございます。今年はもう少し更新するようにしたいです(抱負)。

とはいえC#ネタは欠乏気味で、どのくらいのペースで更新できるか…(弱気)。

ともあれ、本年もよろしくお願いいたします。

2013年8月30日金曜日

なつがおわりまーす

とか言いつつ今日は暑いです。

えー、8月も間もなく終わり、前回のエントリからほぼ3週間ですか。思いの外サボってしまいました…。

そんな時期もありますよね(白目)。ボチボチいきます…。

2013年8月10日土曜日

古くなったログファイルを削除する処理をC#+LINQで。

何かしら業務で利用するシステムであれば、大抵何かあったときの障害解析などに備え、ログファイルが作られることになります。で、そのログファイルは作られっぱなしだとそのうちディスクを圧迫するので、ある一定のルールの下で削除されていくことになるのが一般的だと思います。

その「削除されるルール」は、当然すべてきれいさっぱり消してしまっては、いざという時に役に立たないし、またシステムによってログの残し方の要件が違ったりで、設計や実装も結構厄介なもんです。

ここでは、いくつかログの残し方ルールをあげて、古いログファイルを削除する処理をC#+LINQを使って書いてみようと思います。

ログファイルの構成前提


前提として、ログファイルは以下のような形で残されていることとします。

  • ログフォルダにはログファイルのみが存在する。
  • ログファイルのファイル名は「log_yyyy-MM-dd_nn.log」とする。ただし、yyyyは西暦の4ケタ。MMは月の2ケタ。ddは日の2ケタ。nnはシーケンシャル番号の2ケタ。
  • ログファイルは以下のタイミングで新たに生成される。ただし、同一日のログファイルが存在しない場合はシーケンシャル番号は「0」。存在する場合はシーケンシャル番号の最大+1になるようにする。
    • 日が変わった以降最初のログ出力時。
    • 現在のログファイルが所定のサイズ以上になった時。
    • サービスの起動時。
そしてログファイル削除クラスとしてスタティックな「DeleteOldLogFiles」クラスを作ることとします。

バリエーション1:指定ファイル数のみ残して古いものを削除


指定された数のより新しいファイルを残し、それ以外のものを削除する仕様として処理を考えます。この場合、ファイル名を降順でソートして、新しいほうから所定の数は何もせず、それ以降はファイルを削除する。という処理を考えてみました。

ForEachメソッドを使いたいので、IEnumerableを一旦Listに変換しています。ま、しょうがない。

public static void ByCounts(string path, int keepCount)
{
  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .Skip(keepCount)
    .ToList()
    .ForEach(f => File.Delete(f));
}

ま、シンプルですよね。

バリエーション2:指定ファイルサイズに収まるようにして古いものを削除


バリエーション1の変形ですが、新しいほうからファイルサイズを足して行って、所定のサイズ以下のうちはスキップし、所定のサイズを超えた以降のファイルを削除する処理とするとシンプルに書けますね。

public static void BySize(string path, long size)
{
  long totalSize = 0;

  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .SkipWhile(f => (totalSize += new FileInfo(f).Length) < size)
    .ToList()
    .ForEach(f => File.Delete(f));
}


バリエーション3:日付が古いものを削除


ファイル名から日付部分を抜き出して、指定日数より以前のものは削除する仕様とします。このメソッドの引数「path」と「days」はまあいいとして、「startPos」はファイル名中の日付文字列の開始位置を、「dateForm」は日付文字列の解析フォームを渡します。

public static void ByDays(string path, int days, int startPos, string dateForm)
{
  var target = DateTime.Today.AddDays(-days);

  Directory.GetFiles(path)
    .Where(f => DateTime.ParseExact(
      Path.GetFileName(f).Substring(startPos, dateForm.Length),
      dateForm,
      System.Globalization.DateTimeFormatInfo.InvariantInfo) < target)
    .ToList()
    .ForEach(f => File.Delete(f));
}

今回のケースでログファイルの保持日数を3日とすると、ログファイル名は「log_yyyy-MM-dd_nn.log」なので、「startPos」は4、「dateForm」は「yyyy-MM-dd」となるので、以下のようにこのメソッドを呼び出すことになります。

DeleteOldLogFiles.ByDays(logFolderPath, 3, 4, "yyyy-MM-dd");


2013年8月1日木曜日

今週のマガジン

『あひるの空』が凄かった。なんだこれ?数週間のモヤモヤを一気に晴らす10ページ。一瞬何が起きたのかわからないくらいの一気。いやー。スゲー。
しかし、「どこに負けるか」をバラしておいて、「敗戦までのキセキ」を描くんですか。是非描ききってほしいです。

『ベイビーステップ』も暗い話にならなくて良かった。とはいえバッドエンドの地雷は埋まったままなので、ここからどう展開するのか楽しみ。