2015年11月5日木曜日

今週のモーニング

『ピアノの森』が最終回を迎えました。

連載開始から18年とのこと。ものすごい長期連載が大団円を迎えました。
最初の連載はアッパーズで、当時掲載誌で読んでました。アッパーズは好きだったなぁ。
長らく楽しませていただきました。最終回は、ありゃ泣くよ。

お疲れ様でした。次回作はどんなお話でしょうか。楽しみにしてます。

それと、『決してマネしないでください』が次回で最終回とのこと。これも楽しみだっただけに、正直もう少し続けてほしかったなぁ。

2015年10月22日木曜日

SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。(改)

もともとは「ラムダ式を利用したリファクタリングの例 その2」で扱った後、さらに「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。」で修正版を示したネタです。改二ですな。

こんな要求に対する処理を書いていました。
  • Shift-JISのCSVファイルを入力し、UTF-8のCSVファイルを出力する。
  • 入力したCSVの各カラムは、固定長で前後に空白が入る可能性があり、その空白は除去して出力する。
  • 各行の先頭のカラムはIDになっていて、特定のIDのみ出力対象とする。
inFileが入力ファイルのパス、outFileが出力ファイルのパスとして、絞り込みの条件を「先頭カラムが奇数」とすると、以下のコードで拡張メソッドとか作らなくても要求が満たせてしまいますね。

File.WriteAllLines(
  outFile,
  File.ReadLines(inFile, Encoding.GetEncoding("shift-jis"))
    .Select(line => line.Split(','))
    .Where(items => int.Parse(items[0]) % 2 != 0)
    .Select(items => string.Join(",", items.Select(item => item.Trim()))));

  • inFileからShift-JISで各行を読み込み、
  • ',' で分割し、
  • 先頭カラムが奇数の行について、
  • 各カラムの前後の空白を除去した上で再度 ',' で結合し、
  • outFileに書き込む。

という処理になります。日本語とほぼ一対一にコードが対応してます。ちなみに、.net 2.0相当でコードを書くとこんな感じ。

var sb = new StringBuilder();
string line;

using (var sr = new StreamReader(inFile, Encoding.GetEncoding("shift-jis")))
using (var sw = new StreamWriter(outFile))
{
  while ((line = sr.ReadLine()) != null)
  {
    var items = line.Split(',');
    sb.Length = 0;

    if (int.Parse(items[0]) % 2 != 0)
    {
      foreach (var item in items)
      {
        sb.Append(item.Trim()).Append(',');
      }
      sw.WriteLine(sb.ToString(0, sb.Length - 1));
    }
  }
}

雲泥の差がありますねぇ…。

2015年7月6日月曜日

C#でLEFT OUTER JOIN (左外部結合)

LINQを使って、「LEFT OUTER JOIN」をしたかった。普通のJoinだとINNER JOINなので、少し工夫する必要がありそう。ググってみると、以下のようなコードが一般的なようだ。

var query = from person in people
  join pet in pets on person equals pet.Owner into gj
  from subpet in gj.DefaultIfEmpty()
  select new
  {
    person.FirstName, 
    PetName = (subpet == null ? String.Empty : subpet.Name)
  };

ちなみにこれは、MSDNの「方法 : 左外部結合を実行する (C# プログラミング ガイド)」から。

大体どこを見てもこんな感じで、ほとんど左外部結合をしたいときの慣用句(イディオム)みたいなもののよう。とはいえ、このコードを見て「あ、左外部結合させたいんだな」と思える人がどれくらいいるか。要は、コードから意図が解りづらいので、あまり好みじゃないなぁ。と。

もう少し、コードを見て左外部結合であることが解るように、拡張メソッドを作ってみた。こんなの。

public static class EnumerableEx
{
  public static IEnumerable<TResult> OuterJoin<TOuter, TInner, TKey, TResult>
    (this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector,
    TInner innerDefaultValue)
  {
    return outer
      .GroupJoin(
        inner,
        outerKeySelector,
        innerKeySelector,
        (o, i) => new { Out = o, Ins = i.DefaultIfEmpty(innerDefaultValue) })
      .SelectMany(g => g.Ins, (g, i) => resultSelector(g.Out, i));
  }
}

Enumerable.Joinメソッドと比べると、最後のパラメータが余計についてます。これはOuterの要素に対して合致するInnerの要素がなかった時の代替要素を指定するもの。Null Objectだと思っておけばよいと思います。

あと、名前は長いのを嫌い、「OuterJoin」としています。タイプパラメータからも、左が外なのは明らかなので、特に問題はないと思っています。

ちなみに、これをライブラリ的に用意するなら、GroupJoinするときにEqautityComparerを指定するパターン、それと、DefaultIfEmptyでTInnerのデフォルト値を自動的に使うパターン、その組み合わせで4つのオーバーロードを、用意しておくのがよいと思います。

さて、テストします。

static void Main(string[] args)
{
  var products = new[] 
  {
    new { ProductId = 1, Name = "えんぴつ" },
    new { ProductId = 2, Name = "けしごむ" },
    new { ProductId = 3, Name = "コンパス" },
    new { ProductId = 4, Name = "クレヨン" },
  };

  var sales = new[]
  {
    new { SaleId = 1, ProductId = 1, Buyer = "○○商会", Quantity = 2 },
    new { SaleId = 2, ProductId = 1, Buyer = "××文具店", Quantity = 4 },
    new { SaleId = 3, ProductId = 2, Buyer = "△屋", Quantity = 3 },
    new { SaleId = 4, ProductId = 4, Buyer = "○○商会", Quantity = 1 },
    new { SaleId = 5, ProductId = 5, Buyer = "○○商会", Quantity = 1 },
  };

  var nosaled
    = new { SaleId = 0, ProductId = 0, Buyer = "(no sales)", Quantity = 0 };

  var salesInfo = products.OuterJoin(
    sales, 
    p => p.ProductId, 
    s => s.ProductId, 
    (p, s) => new { p.Name, s.Buyer, s.Quantity }, 
    nosaled);

  foreach (var i in salesInfo)
  {
    Console.WriteLine("{0}, {1}, {2}", i.Name, i.Buyer, i.Quantity);
  }
}

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
コンパス, (no sales), 0
クレヨン, ○○商会, 1

…右外部結合?RIGHT OUTER JOINか。僕自身使ったことないですが、必要なら右左を入れ替えてあげればいいはず。上のコードの20行目から28行目を、こんな感じに置き換えてみる。
var noproduct = new { ProductId = 0, Name = "(no item)" };

var salesInfo = sales.OuterJoin(
  products,
  s => s.ProductId,
  p => p.ProductId,
  (s, p) => new { p.Name, s.Buyer, s.Quantity },
  noproduct);

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
クレヨン, ○○商会, 1
(no item), ○○商会, 1

うん。いんじゃないかな?
…完全外部結合?…FULL OUTER JOIN…。必要?それ。

使い道が解らなくてモチベーションゼロだけど、要するに左外部結合+右のみに存在するレコードを、編集してUnionすればいいんじゃないかな?多分。

2015年7月3日金曜日

GroupByとToLookup

ここしばらく、C#関連記事ではGroupByを使ったものが続いています。

ところで、.net Frameworkには、GroupByと非常によく似た「ToLookup」というEnumerable拡張メソッドがあります。大体名前のイメージで、GroupByは遅延実行で、ToLookupは即時実行だと思い、基本的に使い分けていました。

もう少し調べてみると、ToLookupはその名の通りルックアップテーブルを作るメソッドで、戻されるLookupクラスは、キーからのルックアップを機能として有している。と。

ところがふと、過去にGroupByを使っていて、ToLookupを使ったほうがよいケースがあったりするんじゃないかと不安になってきまして。たとえば何も考えずにGroupByを使っていた箇所で、ToLookupを使ったほうが実はよかった。とかいうケースがあったらいやだなぁ。と。

なので、とりあえず処理時間を計ってみました。使ったのはこんなコード。

static void Main(string[] args)
{
  var rnd = new Random(DateTime.Now.Millisecond);
  var items = Enumerable.Range(0, 10000000).Select(_ => new
  {
    Key1 = rnd.Next(10),
    Key2 = rnd.Next(100),
    Value = rnd.Next(10000)
  });

  var sw = new Stopwatch();
  sw.Start();

  var dic = items.GroupBy(i => new { i.Key1, i.Key2 })
    .ToDictionary(g => g.Key, g => g.First().Value);

  sw.Stop();

  Console.WriteLine("Elapsed:{0}msec", sw.ElapsedMilliseconds);
}

先日の『ToDictionaryで重複のない辞書を作る』と同様に、コレクションからキー重複を排除した辞書を作る処理にGroupByを使ってみて、単純にGroupBy→ToLookupに置き換えたときにどれくらい処理速度の差があるか?ほんの少しGroupByが早いんじゃないかな?と予想。

結果は、
GroupBy … 6155msec。
ToLookup … 6081msec。
となり、予想に反してほんの誤差レベルとはいえGroupByのほうが遅い結果に。なんでだろう?と思ってソースコードを調べてみた。

GroupByは大体こんな感じ。

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return new GroupedEnumerable<TSource, TKey, TSource>(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

internal class GroupedEnumerable<TSource, TKey, TElement>
  : IEnumerable<IGrouping<TKey, TElement>>
{
  public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
  {
    return Lookup<TKey, TElement>.Create<TSource>(
      source, keySelector, elementSelector, comparer).GetEnumerator();
  }
}

それに対して、ToLookupはこんな感じ。

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return Lookup<TKey, TSource>.Create(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

あー。これだとほぼ即時実行か遅延実行かの違いしかないですねぇ。これならToLookupが若干速いのも納得できる。

というか、そうするとGroupByの存在意義が解らなくなったので、ちょっと調べてみたら、Stackoverflowに「Why are ToLookup and GroupBy different?」こんな質問があって、その回答が以下。
What happens when you call ToLookup on an object representing a remote database table with a billion rows in it?
なるほど。例えばデータソースがObjectではなくDatabaseだったりしたときに、即時実行しないで済むならそうしたい。LINQのメソッドはデータソースを問わないわけだから、基本的にはGroupByを使い、明示的に即時実行にしたい場合や、実行後のルックアップが必要なら、ToLookupを使うべし。と、解釈して納得できた。

気にしなければならない速度差では全くないですしね。

2015年6月30日火曜日

『月光~the lunatic~』

イブニングで5週掲載された、ウチヤマユージさんの『月光~the lunatic~』の単行本が発売されたようなので、おススメを。



表題作と他一遍の中編集。イブニング掲載時に読みましたが、すごく良かった。絵も話もとても好み。

静かに始まる第一話。一見他愛ない日常に見えて、ちらほらと異常がちりばめられる。
話が進むと時間軸を行ったり来たりしながら展開していき、少しずつ全容が見えてくる。
すべてが明らかになった後、結末は「んなあほな」と思いつつ、なんだか妙に晴れやかな気分。

この作者さんは知りませんでしたが、今後に期待大です。

2015年6月25日木曜日

『僕だけがいない街』アニメ化決定

『僕だけがいない街』の最新刊の発売はいつかな?と、調べていたらこんなのを見つけました。

『僕だけがいない街』アニメ化決定&最新刊発売!各界の著名人も絶賛

え?アニメ化?まじで?だって、未完結の状態でこれアニメになんかできないでしょ?と思いつつ、記事を読むと、『2016年1月フジテレビ「ノイタミナ」枠でアニメスタート』とあった。

これは全部妄想だけど、アニメが2クールだとすると、2016年6月にアニメが最終回になるわけで。そうすると、2015年7月に単行本6巻が発売で、1冊出るのに半年かかるので、2016年1月に7巻、2016年7月に8巻が、最終巻として出るなら帳尻は合う。一回くらい休載しても何とか間に合うか。

『四月は君の嘘』もそんな感じじゃなかったっけ?あれもノイタミナ。

そーすると、あと12話で完結??


…なんてことを考えながら、はやく6巻読みたいな。単行本で追いかけてるので、続きが気になって。

2015年6月22日月曜日

ToDictionaryで重複のない辞書を作る

シンプルなTipsをひとつ。

LINQのToDictionaryメソッドは、コレクションから辞書を簡単に作ってくれる、使用頻度の高い便利なメソッドです。ただ、指定したキーセレクタの結果、キーが重複していると例外(ArgumentException)をスローします。例えば、以下のようなコード。

var collection = new [] { "One", "Two", "Three", "Four" };
var dic = collection.ToDictionary(c => c.First());

この例では、文字列の最初の文字をキーにするので、キー'T'が重複します。しかし、キーに対して最初に現れた値を登録するような、単純なルールで重複を排除して辞書を作りたいケースがあります。

さて、どう解決しましょうか。ToDictionaryのメソッドで、重複を排除するようなオーバーロードがあれば簡単ですが、ないですね。なので、こんなコードではどうでしょうか?

var collection = new [] { "One", "Two", "Three", "Four" };
var dic = collection.GroupBy(c => c.First())
    .ToDictionary(g => g.Key, g => g.First());

ToDictionaryの前に、一旦最初の文字でグループ化しておいて、そのグループキーと最初の値を辞書のエントリとしてみました。