TDictionaryをフィルタリング&ソートする

ども、Norimakiです。

久しぶりにDelphiのお話です。

Delphiでデータを扱う際にTDictionaryとかTListなどをよく使います。
で、TDictionaryは検索が効率的だということで、そっちを使いたいんですが、
ソートが出来ないという問題にぶち当たります。

そもそもTDictionaryはソートするもんじゃないぜ。

と言われそうですが、それでもソートしたい時ってのがあるわけで。

じゃあどうするか?

データをTDictionaryに持たせて、順番をTListに持たせる。

という方法でどうだと。

「順番をTListに持たせる」というのは何か気持ち悪い表現ですが、
それはおいておいて、まず、TDictionayのキーをTListに保存するわけですね。
for-in-doを使ってキーを抜き出してTListに保存します。

ここで、必要に応じてフィルタリングをするのも良いでしょう。

で、TListはソートできますから、ソートの際にはTListのソートをつかうと。

TDictionayのキーはどういう型であれ一意ですから、
TListとTDictionaryは1対1で対応しています。

ということで、サンプルがこちら。

procedure TForm1.TDictionaryFilterSort;

 procedure DispStatus(Str:string);
 begin
  StatusBar1.SimpleText:=Str;
  StatusBar1.Update;
 end;

 function TimerStart:Cardinal;
 begin
  timeBeginPeriod(1);
  Result:=timeGetTime;
 end;

 function TimerEnd(st:cardinal):Cardinal;
 begin
  Result:=timeGetTime-st;
  timeEndPeriod(1);
 end;

 function GetTimerMemo(t:cardinal;str:string):string;
 begin
  Result:=Str+' '+FormatFloat('#,##0 msec',t)+#13#10;
 end;


type
 TNYSampleRec=record
  Number:integer;
 end;


var Dic:TDictionary<integer,TNYSampleRec>;
    MyList:TList<integer>;
    i,id:integer;
    Rt:TNYSampleRec;
    s,u:string;
    t:Cardinal;

    LoopCnt :integer;
    JudgeCnt:integer;
begin

 LoopCnt :=SpinEdit2.Value;
 JudgeCnt:=SpinEdit3.Value;

 u:='';

 Dic   :=TDictionary<integer,TNYSampleRec>.Create;
 MyList:=TList<integer>.Create;

 try
  DispStatus('ダミーデータ作成');
  t:=TimerStart;
  for i:=1 to LoopCnt do begin
   Rt.Number:=Random(100);
   Dic.Add(i,Rt);
  end;
  t:=TimerEnd(t);
  u:=u+GetTimerMemo(t,'ダミーデータ作成');


  DispStatus('フィルタリング');
  t:=TimerStart;
  for i in Dic.Keys do begin
   if Dic[i].Number>JudgeCnt then continue;
   MyList.Add(i);
  end;
  t:=TimerEnd(t);
  u:=u+GetTimerMemo(t,'フィルタリング');


  DispStatus('ソート');
  t:=TimerStart;
  MyList.Sort(
   TComparer<integer>.Construct(
    function(const ID1,ID2:integer):integer
    begin
     Result:=Dic[ID1].Number-Dic[ID2].Number;
     if Result=0 then Result:=ID1-ID2;
    end
   )
  );
  t:=TimerEnd(t);
  u:=u+GetTimerMemo(t,'ソート');

  DispStatus('表示データ作成');
  t:=TimerStart;
  Memo2.Clear;
  Memo2.Lines.BeginUpdate;
  s:='';
  try
   for i:=0 to MyList.Count-1 do begin
    id:=MyList[i];
    //Memo2.Lines.Add(IntToStr(id)+' '+IntToStr(Dic[id].Number));
    s:=s+IntToStr(id)+' '+IntToStr(Dic[id].Number)+#13#10;

    if (i mod 10000)=0 then DispStatus('表示データ作成 '
                             +IntToStr(i)+'/'+IntToStr(MyList.Count));
   end;
   t:=TimerEnd(t);
   u:=u+GetTimerMemo(t,'表示データ作成');


   DispStatus('表示データ転送');
   t:=TimerStart;
   Memo2.Lines.Text:=s;
   t:=TimerEnd(t);
   u:=u+GetTimerMemo(t,'表示データ転送');

   Memo2.Lines.Add(#13#10+u);

  finally Memo2.Lines.EndUpdate;
  end;


 finally FreeAndNil(Dic);
         FreeAndNil(MyList);
 end;

 DispStatus('');
end;

こんな感じで。

usesに、Generics.Collections,Generics.Defaults,MMSystem
を追加して下さい。

フォームには、

・SpinEdit2
・SpinEdit3
・StatusBar1
・Memo2

コンポーネントを設置して下さい。
StatusBar1のSimplePanelプロパティはTrueに設定。

SpinEdit2にダミーデータの個数。
SpinEdit3にフィルタリングの数。

を設定します。

フィルタリングについては、
ダミーデータとしてランダムに0から99までの数字が設定され、
設定数以下の数字を抽出する。という仕組みになっています。

面倒なら、ソース中の

LoopCnt :=SpinEdit2.Value;
JudgeCnt:=SpinEdit3.Value;

に適当な数字を入れて下さい。

テスト的には、
LoopCntに1000000、JudgeCntに50をいれてみるといいのではないかと。

うちの環境では、上記設定で

ダミーデータ作成 282 msec
フィルタリング 128 msec
ソート 2,893 msec
表示データ作成 169 msec
表示データ転送 1,965 msec

という感じになりました(詳しい環境状況はよくわかりません)。

あとは、ジェネリクスを使ったり、ソート・フィルタリング部を
分離したりして一般化して使いやすくするとどうかなと。

これが使えるかどうか、意味があるかどうかはわかりませんが、
こういうやり方もありかなと。

ではでは。
Norimakiでした。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする