質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

1回答

2410閲覧

【C#】LINQを使用したグループ化について

inari_ken

総合スコア34

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2019/01/29 02:27

編集2019/01/29 12:45

こんにちは。
今回もよろしくお願いいたします。

前提・実現したいこと

LINQにて各データをグループ化してからコンボボックスに格納するプログラムを作成しました。
ここから更にcmbZOK1(コンボボックス)から項目を選択したタイミングで、”cmbZOK1の選択項目が含まれる行のデータ”を抽出・グループ化し、cmbZOK2(コンボボックス)以降に表示したいです。

発生している問題・エラーメッセージ

関数”cmbZOK1_SelectedIndexChanged”内の"groupby~"部分が問題なのではと考えています。どのように条件を設定すれば正しく抽出できますか?
現時点では下記で抽出条件を設定したつもりですが、結果はAddCmbを動作させたときと同じ内容で表示されてしまいます

該当のソースコード

C#

1 public MAIN() 2 { 3 InitializeComponent(); 4 5 6 // テキストデータ読み込み 7 string file = @"C:aaa.csv"; 8 StreamReader sr = new StreamReader(file, Encoding.GetEncoding("SHIFT_JIS")); 9 while (sr.EndOfStream == false) 10 { 11 string line = sr.ReadLine(); 12 string[] linef = line.Split(','); 13 F.Add(linef); 14 15 } 16 sr.Close(); 17 18 // 属性コンボボックスにグループ化した属性をAdd 19 AddCmb(cmbZOK1, 0); // cmbZOK1 20 AddCmb(cmbZOK2, 1); // cmbZOK2 21 AddCmb(cmbZOK3, 2); // cmbZOK3 22 AddCmb(cmbZOK4, 3); // cmbZOK4 23 AddCmb(cmbZOK5, 4); // cmbZOK5 24 AddCmb(cmbZOK6, 5); // cmbZOK6 25 AddCmb(cmbZOK7, 6); // cmbZOK7 26 AddCmb(cmbZOK8, 7); // cmbZOK8 27 AddCmb(cmbZOK9, 8); // cmbZOK9 28 AddCmb(cmbZOK10, 9); // cmbZOK10 29 30 void AddCmb(ComboBox ControlName, int arr) 31 { 32 var query = F.GroupBy(x => x[arr]); 33 foreach (var group in query) 34 { 35 ControlName.Items.Add(group.Key); 36 } 37 } 38 39 } 40 41 42 private void cmbZOK1_SelectedIndexChanged(object sender, EventArgs e) 43 { 44 var query = F.GroupBy(x => new { x = cmbZOK1.Text, y = x[1] }); 45 cmbZOK2.Items.Clear(); 46 foreach (var group in query) 47 { 48 cmbZOK2.Items.Add(group.Key.y); 49 } 50 } 51 52

補足情報

取り込むcsvの内容ですが、下記のような形です。

列1列2列3・・・
エチオピアグジナチュラル
エチオピアグジウォッシュト
エチオピアシダモナチュラル
エチオピアシダモウォッシュト
コロンビアナリーニョナチュラル
コロンビアナリーニョウォッシュト

列1(cmbZOK1)で”エチオピア”と選択した場合、列2(cmbZOK2)はグジ、シダモが抽出され、列3にはナチュラル、ウォッシュトが抽出されるイメージです。

今後、列2で項目を選択した場合、列3以降は列1,2で選択した条件で抽出されるようにしたいと考えています。

ソースコード修正後(自分用)

ご回答を元にソースコードを修正し、意図通りに動作しました。

c#

1 public partial class MAIN : Form 2 { 3 4 // クラス内public 5 private List<string[]> F = new List<string[]>(); 6 private List<ComboBox> Cmb = new List<ComboBox>(); //cmbboxを共通関数で扱いやすくするため、リストに入れて覚えておくためのもの 7 8 public MAIN() 9 { 10 InitializeComponent(); 11 12 13 // テキストデータ読み込み 14 string file = @"C:\txt.csv"; 15 StreamReader sr = new StreamReader(file, Encoding.GetEncoding("SHIFT_JIS")); 16 while (sr.EndOfStream == false) 17 { 18 string line = sr.ReadLine(); 19 string[] linef = line.Split(','); 20 F.Add(linef); 21 22 } 23 sr.Close(); 24 25 // 属性コンボボックスにグループ化した属性をAdd 26 AddCmb(cmbZOK1, 0); // cmbZOK1 27 AddCmb(cmbZOK2, 1); // cmbZOK2 28 AddCmb(cmbZOK3, 2); // cmbZOK3 29 AddCmb(cmbZOK4, 3); // cmbZOK4 30 AddCmb(cmbZOK5, 4); // cmbZOK5 31 AddCmb(cmbZOK6, 5); // cmbZOK6 32 AddCmb(cmbZOK7, 6); // cmbZOK7 33 AddCmb(cmbZOK8, 7); // cmbZOK8 34 AddCmb(cmbZOK9, 8); // cmbZOK9 35 AddCmb(cmbZOK10, 9); // cmbZOK10 36 37 void AddCmb(ComboBox ControlName, int arr) 38 { 39 var query = F.GroupBy(x => x[arr]); 40 foreach (var group in query) 41 { 42 ControlName.Items.Add(group.Key); 43 } 44 Cmb.Add(ControlName); //★ここでzok1から10をaddしておく。 45 } 46 47 } 48 49 private void Form1_Load(object sender, EventArgs e) 50 { 51 52 53 } 54 55 private void cmbZOK1_SelectedIndexChanged(object sender, EventArgs e) 56 { 57 Handler(cmbZOK1.Text, 0); 58 } 59 60 private void cmbZOK2_SelectedIndexChanged(object sender, EventArgs e) 61 { 62 Handler(cmbZOK2.Text, 1); 63 } 64 65 private void cmbZOK3_SelectedIndexChanged(object sender, EventArgs e) 66 { 67 Handler(cmbZOK3.Text, 2); 68 } 69 70 private void cmbZOK4_SelectedIndexChanged(object sender, EventArgs e) 71 { 72 Handler(cmbZOK4.Text, 3); 73 } 74 75 private void cmbZOK5_SelectedIndexChanged(object sender, EventArgs e) 76 { 77 Handler(cmbZOK5.Text, 4); 78 } 79 80 private void cmbZOK6_SelectedIndexChanged(object sender, EventArgs e) 81 { 82 Handler(cmbZOK6.Text, 5); 83 } 84 85 private void cmbZOK7_SelectedIndexChanged(object sender, EventArgs e) 86 { 87 Handler(cmbZOK7.Text, 6); 88 } 89 90 private void cmbZOK8_SelectedIndexChanged(object sender, EventArgs e) 91 { 92 Handler(cmbZOK8.Text, 7); 93 } 94 95 private void cmbZOK9_SelectedIndexChanged(object sender, EventArgs e) 96 { 97 Handler(cmbZOK9.Text, 8); 98 } 99 100 private void cmbZOK10_SelectedIndexChanged(object sender, EventArgs e) 101 { 102 Handler(cmbZOK10.Text, 9); 103 } 104 105 // Fから特定列(縦方向)を抜き出す補助関数 106 static IEnumerable<string> PickColumn(IEnumerable<string[]> table, int columnNumber) => table.Select(line => line[columnNumber]); 107 108 // Fから、idx列目までの値を基準に、その手前までを絞る補助関数 109 static IEnumerable<string[]> FilterMulti(IEnumerable<string[]> table, List<ComboBox> cmbboxes, int selectedCmbIdx) => table 110 .Where(line => Enumerable.Range(0, selectedCmbIdx + 1) 111 .All(i => cmbboxes[i].Text == "" || line[i] == cmbboxes.ElementAt(i).Text) ); 112 113 //cmb内容をリセット・グループ化するための補助関数 114 static void ResetItems(ComboBox cmb, IEnumerable<string> items) //★objectだとgroupbyが使えないため、stringに変更 115 { 116 cmb.Items.Clear(); 117 var query = items.GroupBy(x => x); 118 foreach(var group in query) 119 { 120 cmb.Items.Add(group.Key); 121 } 122 123 } 124 125 //CmbZOK1_selectedIndexChangeとかで、このハンドラを呼び、そのとき、Zok1なら0,Zok8なら7という自身のcmbにおけるインデクスと、選択された値を渡してください。 126 void Handler(string selectedText, int selectedCmbIdx) 127 { 128 var filteredF = F.Where(x => x[selectedCmbIdx] == selectedText).ToArray(); 129 var filteredMultiF = FilterMulti(filteredF, Cmb, selectedCmbIdx); 130 131 for (var i = selectedCmbIdx + 1; i < 10; i++) 132 { 133 ResetItems(Cmb[i], PickColumn(filteredMultiF, i)); 134 Cmb[i].Text = string.Empty; 135 } 136 } 137 138 }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

10列あることを考えると、これで使いものになるかは疑問ですが、現状を打破するには↓こうではないかと。

csharp

1 private void cmbZOK1_SelectedIndexChanged(object sender, EventArgs e) 2 { 3 var query = F.Where(x=>x[0] == cmbZOK1.Text).GroupBy(x => x[1]);// 0がマッチするので絞ってやってから、最初と同じことすりゃいいのでは? 4 cmbZOK2.Items.Clear(); 5 foreach (var group in query) 6 { 7 cmbZOK2.Items.Add(group.Key.y); 8 } 9 }

では、5が選ばれたときに、1から4を""にし、6から10が5のみに基づいて絞られるような集約した例をご提示します

csharp

1 public Form1() 2 { 3 InitializeComponent(); 4 Cmb.Add(new ComboBox());//ここでzok1から10をaddしておきます。 5 Cmb.Add(new ComboBox()); 6 } 7 private List<string[]> F = new List<string[]>(); //Fはこういう型だと想定してます 8 private List<ComboBox> Cmb = new List<ComboBox>();//cmbboxを共通関数で扱いやすくするため、リストに入れて覚えておくためのもの 9 10 //Fから特定列(縦方向)を抜き出す補助関数 11 static IEnumerable<string> PickColumn(IEnumerable<string[]> table, int columnNumber) => table.Select(line => line[columnNumber]); 12 13 //addcmbみたいな、リセットするための補助関数(たしかaddrangeだと追加だけだったように思う) 14 static void ResetItems(ComboBox cmb, IEnumerable<object> items) 15 { 16 cmb.Items.Clear(); 17 cmb.Items.AddRange(items.ToArray()); 18 } 19 20 //CmbZOK1_selectedIndexChangeとかで、このハンドラを呼び、そのとき、Zok1なら0,Zok8なら7という自身のcmbにおけるインデクスと、選択された値を渡してください。 21 void Handler(string selectedText, int selectedCmbIdx) 22 { 23 var filteredF = F.Where(x => x[selectedCmbIdx] == selectedText).ToArray(); 24 for (var i = selectedCmbIdx + 1; i < 10; i++) 25 { 26 ResetItems(Cmb[i], PickColumn(filteredF, i)); 27 } 28 for (var j = 0; j < selectedCmbIdx; j++) 29 { 30 Cmb[j].Text = string.Empty; 31 } 32 }

さらに追加
Fから、idx列目までの値を基準に、その手前までを絞る方法。ただこの場合、手前に空文字があるとうまく絞れないので、その辺手当が必要

csharp

1 static IEnumerable<string[]> FilterMulti(IEnumerable<string[]> table, IEnumerable<ComboBox> cmbboxes, int selectedCmbIdx) => table.Where(line => Enumerable.Range(0, selectedCmbIdx + 1).All(i => line[i] == cmbboxes.ElementAt(i).Text));

投稿2019/01/29 02:49

編集2019/01/29 04:33
papinianus

総合スコア12705

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

papinianus

2019/01/29 02:58

全部が連動するようにした場合、初期状態から1を選択しただけで、2だけでなく3から10までが絞られる。 逆に次にしか連動しないことにしてしまうと、2を選択したあと3が絞られるが、そのときの4は1,2,3の組み合わせに存在しない選択値となっている可能性がある。 正しく動く仕様を決めるのが難しいです。
inari_ken

2019/01/29 03:26

ご回答ありがとうございます。 最終的には、1を選択した時点で2~10まで絞られる形にしたいです。 データの特性上、左列にいくほど大枠になりますので、5のみを絞った場合は、1~4までは空欄とし、6以降が抽出されるようになれば完璧です。 わかりづらい質問内容で申し訳ありません。 Whereで行うとなると、2~10のchangedの関数の処理が複雑になってしまうような気がします。
papinianus

2019/01/29 03:29 編集

利用状況が分からないです。 > 5のみを絞った場合は、1~4までは空欄 生真面目に、コロンビア、ナリーニョ、ナチュラル、と左から絞ってきたのに、4つめを選択するやいなや、この3つが選択していなかった状態に戻る動きはユーザとして不満しか感じないです。 5のみを絞るのか、1から5を絞るのかを、コンボボックスでどう表現する予定なのか伺えますか?
papinianus

2019/01/29 03:32

私としては、Whereを使うから複雑になるとは認識してません。選択されたものによって、参照する元(2なら1,3なら2(および1?))と、連動する先が個数も要素も異なることが複雑さの原因です。この構造上の複雑さが、GroupByでWhereより簡単になるとは思えないです。
inari_ken

2019/01/29 04:18

> 5のみを絞った場合は、1~4までは空欄 説明不足ですみません。上記は1~4を選択せず、いきなり5から抽出を始めた場合の動作となります。 この場合、6以降は"5で選択した項目に含まれる行"の条件で抽出されます 初心者にもかかわらず、コーディングを行わずwhereで行うと複雑~などと発言してしまったのは失礼しました。一度試してみます。 仕様通りに実装できればよいので、LINQを使用しない別の方法も募集しています。(欲をを言えば10万件程度はデータが入ってくると想定しているので、処理速度が速いものがよいですが)
papinianus

2019/01/29 04:34 編集

いくつかの補助メソッドを用意することで、whereを使っても特段行数が膨れあがることなく、処理を書くことができる例を追記させていただきました。 ただ、やはり、この機構のユーザ体験が好ましいとは思えず、動作仕様を再度ご検討なさるようお願いしたいです。
papinianus

2019/01/29 04:23

すみません。修正にあたって、コメントを読まずに作ってしまいました。 全部連動するとなると、また違う取り組みが必要です。ただ、先に書いたように、初期状態から3を選ぶやいなや初期の1と2をもとに、4から10が確定してしまう動作になると予想されます。 Whereを入れると複雑にはなります。ただそれはGroupByによってではなく、回答例のように他のヘルパメソッドやcombboxがリストになっていることで解決できる問題だと思います。 (私は初心者だから間違いだとも思いませんし、現に間違っていませんし、まったく失礼ではないです。意図を説明しきれていない自分に落ち度があります) 速度についてはあまり考慮していません。10万件ですか。ListやIEnumerableではなく配列にしたり、途中をキャッシュしたほうがいいかもしれませんね。
papinianus

2019/01/29 04:35

手前までを参照しつつ絞るFilterMultiも追加しました。コメントに書いてますが、Allの中で`cmbbox[i].Text == string.Emtpy ||`を入れるなどして、空文字対策が必要です。
inari_ken

2019/01/29 04:40

手厚く対応して頂きありがとうございます。 これから読み込み、プログラムに落とし込んでいきます。取り急ぎ御礼まで。
papinianus

2019/01/29 04:47

不明点などあればご遠慮なくビシビシつっこんでください。それで鍛えられるのが、ここで回答している大きなメリットなんで。
inari_ken

2019/01/29 12:46

悪戦苦闘の末、なんとか意図通りに動作するプログラムが出来ました。 お力添え頂きありがとうございました。 質問欄に追加で修正後のプログラムを載せておきます。 今回、修正中、個人的に詰まった点について、下記に書き出しましたので お時間があればアドバイスをお願いします。 ①ResetItems関数の引数itemsの型を IEnumerable<object> → IEnumerable<string>に  変更しました  →これは型objectだとgroupbyが使えなかったためです。object型のままgroupbyって使えるもの   なのでしょうか。   ②FilterMulti関数の引数cmbboxesの型をIEnumerable→Listへ変更しました。  →関数呼び出しの際に、キャストがうまく出来なかったためです。   動かないだろうと思いながらも型をListに変えてみたところ、正常に動作したので   なぜ動作するのか知りたいところです。   (IEnumerable型でないとcmbboxes.ElementAtは動作しないと思ってました) ③修正後のプログラムから、簡略化できる点・まずい書き方をしている 等がありましたら  簡単でよいのでご指摘いただければと思います
papinianus

2019/01/30 01:18

1. 「使えない」コンパイルエラーにはならないと思うので、意図した動きにならないという意味だと思いますが、どういう状況でしょう。一般論で言えば使えます。 1-1.そう書いたらもうその関数はResetItemsではないです。例えばResetDistinctItemsとかになります。 確かに私は重複を見落していました。そうしたければ`ResetItems(Cmb[i], PickColumn(filteredMultiF, i).Distinct());`でよいはず。重複を排除するためにGroupByをするのは重すぎます(10万件もあるなら) 仮に関数側で処理したいのだとしてもジェネリックにして型制約を置き、Distinctすれば意図としても動作としても、重複排除ができるはずです。 ``` //cmb内容をリセット・グループ化するための補助関数 static void ResetDisinctItems<T>(ComboBox cmb, IEnumerable<T> items) where T : IEquatable<T> { cmb.Items.Clear(); var query = items.Distinct(); foreach(var group in query) { cmb.Items.Add(group.Key); } } ``` 2. キャストをする必要はないはずです。 List<T>はIEnumerable<T>を実装していますので、IEnumerable<T>にあるElementAt()を使えるのは当然です。これがインターフェイスというものの働きです(これが同時にキャストが不要である理由でもあります。この辺はオブジェクト指向とか共変性とか反変性とかもからんでくるのでここで説明するのは私には無理です)。また実体がListであればElementAtはcmb[i]のようなインデクサ演算で動作するはずです。また関数のシグネチャをListにするなら、ElementAtはやめて明示的に[]にしたほうがいいと思います(実際、速度も気にしておられるので、Listにするのはそれほど間違った選択ではないと思います) 3. 速度も気にしているみたいなので、IEnumerableじゃなくて、Listやまた個数が決まっているものは配列にするなど適宜調整したほうがいいと思います(私はどちらかというと汎用的に書いてました)。ただ速さのときは必ず計測してください。速さに影響しないのに改変して汎用性を落とすのは良くないので。 あと、イベントハンドラですが、 private void boxChangeCommon(object sender, EventArgs e) { Handler((Combobox)sender.Text, (Combobox)sender.Tag); } みたいにsenderを使ってcomboboxを参照し、数値は、Tagに事前に保存しとくか、名前の末尾をConvertToIntしたりして、取得するようにすれば、同じようなハンドラを10個も作らなくてすみます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問