none
LINQで特別な条件をつけて複数ソートしたい。 RRS feed

  • 質問

  • LINQを使用してソートを考えているのですが、どう考えてもよく分からなかったので教えてください。

    図のようなことをやりたいのですが、最後のやり方もしくはまとめてやる方法が分かりません。

    ※訂正:②のAの塊内でCのソートは行わない。は誤りで、Bの塊内でCのソートは行わない。です。


    class sortData
    {
       int a;
       int b;
       int c;
    }
    
    List<sortData> list = dtlist.OrderBy(x=>x.a)
                                          .ThenBy(x=>x.b)
                                          .ThenBy(x=>x.c).ToList();

    上記のようにすれば取り敢えずは①のような形になるような気がするのですが、そこからがよく分かりません。

    GroupByで固めればいけるかなと思いましたが、さっぱりでした。

    よろしくおねがいします。


    • 編集済み mogja 2018年6月2日 10:25
    2018年6月2日 10:21

回答

  • 質問するときは最低限のマナーとして、開発環境および実行環境に関する情報(OS、IDE、.NETのバージョンなど)を記載するようにしてください。開発環境のバージョンによってC#言語規格のサポート状況などに違いがあるため、回答のしやすさに影響します。場合によっては最適解が変わることがあります。

    ひとまずVisual Studio 2015以降および.NET 4.5以降の環境であると仮定します。また、質問文に訂正が記載されてありますが、少なくとも期待される結果の例に関しては画像中のテーブルが正しいものであると仮定します。そのほか、丸囲み数字(2)の吹き出しが画像中に2つあるようですが、右上にあるほうは本来(3)であると仮定します。

    期待される結果の例を見る限り、単純にAでグループ化するのではなく、AとBのペアでグループ化する必要があるのではないですか?

    グループ化した後に、Cをキーとしてソートし、さらに各グループ内のBおよびCの最小要素でグループをソートすればよいのではないでしょうか。

    class SortData
    {
      public SortData(int a, int b, int c) { A = a; B = b; C = c; }
      public int A { get; private set; }
      public int B { get; private set; }
      public int C { get; private set; }
    };
    
    static void DoLinqSortTest()
    {
      var srcTable = new List<SortData>();
      srcTable.AddRange(Enumerable.Range(0, 5).Select(x => new SortData(1, 10, 12 - x)));
      srcTable.AddRange(Enumerable.Range(0, 5).Select(x => new SortData(2, 20, 12 - x)));
      srcTable.AddRange(Enumerable.Range(1, 9).Select(x => new SortData(2, 10, x)));
      srcTable.Add(new SortData(1, 20, 7));
      Console.WriteLine("Before:");
      Action<SortData> printData = d => Console.WriteLine($"{d.A}, {d.B}, {d.C}");
      srcTable.ForEach(d => printData(d));
      Console.WriteLine("After:");
    #if false
      var groups1 = srcTable.OrderBy(d => d.A).ThenBy(d => d.B).ThenBy(d => d.C).GroupBy(d => new Tuple<int, int>(d.A, d.B));
    #else
      var groups1 = srcTable.GroupBy(d => new Tuple<int, int>(d.A, d.B), (key, value) => value.OrderBy(d => d.C));
    #endif
      var groups2 = groups1.OrderBy(g => g.First().B).ThenBy(g => g.First().C);
      var resultTable = groups2.SelectMany(g => g.Select(d => d)).ToList();
      resultTable.ForEach(d => printData(d));
    }

    上記コードは提示された期待結果と同じものを出力することだけを検証しています。実際に所望されている仕様であるかどうかは保証できません。LINQの上級者であれば、あるいはもっと効率的なコードを書けるかもしれません。

    ちなみにC# (.NET) では慣習として、型名の先頭を大文字にします。また、classのフィールドは公開せず、通例プロパティなどのアクセッサーを用意します。

    ところで、なぜこのようなソートが必要なのでしょうか? 「XY問題」に陥っていないですか? 単なるアルゴリズム実装手法やLINQの研究目的であれば話は別ですが、根本的な目的が別にあるのであれば、そちらを提示したほうがより適切な回答を得られるケースもあります。

    「XY 問題」とは何ですか? - スタック・オーバーフローMeta

    • 編集済み sygh 2018年6月5日 17:48
    • 回答としてマーク mogja 2018年6月6日 5:37
    2018年6月5日 16:42

すべての返信

  • 質問するときは最低限のマナーとして、開発環境および実行環境に関する情報(OS、IDE、.NETのバージョンなど)を記載するようにしてください。開発環境のバージョンによってC#言語規格のサポート状況などに違いがあるため、回答のしやすさに影響します。場合によっては最適解が変わることがあります。

    ひとまずVisual Studio 2015以降および.NET 4.5以降の環境であると仮定します。また、質問文に訂正が記載されてありますが、少なくとも期待される結果の例に関しては画像中のテーブルが正しいものであると仮定します。そのほか、丸囲み数字(2)の吹き出しが画像中に2つあるようですが、右上にあるほうは本来(3)であると仮定します。

    期待される結果の例を見る限り、単純にAでグループ化するのではなく、AとBのペアでグループ化する必要があるのではないですか?

    グループ化した後に、Cをキーとしてソートし、さらに各グループ内のBおよびCの最小要素でグループをソートすればよいのではないでしょうか。

    class SortData
    {
      public SortData(int a, int b, int c) { A = a; B = b; C = c; }
      public int A { get; private set; }
      public int B { get; private set; }
      public int C { get; private set; }
    };
    
    static void DoLinqSortTest()
    {
      var srcTable = new List<SortData>();
      srcTable.AddRange(Enumerable.Range(0, 5).Select(x => new SortData(1, 10, 12 - x)));
      srcTable.AddRange(Enumerable.Range(0, 5).Select(x => new SortData(2, 20, 12 - x)));
      srcTable.AddRange(Enumerable.Range(1, 9).Select(x => new SortData(2, 10, x)));
      srcTable.Add(new SortData(1, 20, 7));
      Console.WriteLine("Before:");
      Action<SortData> printData = d => Console.WriteLine($"{d.A}, {d.B}, {d.C}");
      srcTable.ForEach(d => printData(d));
      Console.WriteLine("After:");
    #if false
      var groups1 = srcTable.OrderBy(d => d.A).ThenBy(d => d.B).ThenBy(d => d.C).GroupBy(d => new Tuple<int, int>(d.A, d.B));
    #else
      var groups1 = srcTable.GroupBy(d => new Tuple<int, int>(d.A, d.B), (key, value) => value.OrderBy(d => d.C));
    #endif
      var groups2 = groups1.OrderBy(g => g.First().B).ThenBy(g => g.First().C);
      var resultTable = groups2.SelectMany(g => g.Select(d => d)).ToList();
      resultTable.ForEach(d => printData(d));
    }

    上記コードは提示された期待結果と同じものを出力することだけを検証しています。実際に所望されている仕様であるかどうかは保証できません。LINQの上級者であれば、あるいはもっと効率的なコードを書けるかもしれません。

    ちなみにC# (.NET) では慣習として、型名の先頭を大文字にします。また、classのフィールドは公開せず、通例プロパティなどのアクセッサーを用意します。

    ところで、なぜこのようなソートが必要なのでしょうか? 「XY問題」に陥っていないですか? 単なるアルゴリズム実装手法やLINQの研究目的であれば話は別ですが、根本的な目的が別にあるのであれば、そちらを提示したほうがより適切な回答を得られるケースもあります。

    「XY 問題」とは何ですか? - スタック・オーバーフローMeta

    • 編集済み sygh 2018年6月5日 17:48
    • 回答としてマーク mogja 2018年6月6日 5:37
    2018年6月5日 16:42
  • 回答有難うございます。

    環境はVisualStudio2012 .Net4.5以降となります。

    参考処理を変更し確認してみました所、期待する動きとなりました。有難うございます。

    XY問題を確認しましたが、XY問題ではないです。

    A,B,C,Dというように同じ構造のデータが複数箇所で管理されており、

    それらは基本的につなげると1~100のように一連のデータになるのですが、

    A:1~10 B:19~30 C:11~20 D:29~40というように、AからDの順は不動で、

    混在する時がありますので

    今回のようなソートが必要となりました。

    ありがとうございました。


    • 編集済み mogja 2018年6月6日 5:41
    2018年6月6日 5:41