DataTableのソート

ADO.NETを使っていてDataTableがお手軽なメモリデータベースとして使えることに気づいて好んで使っているのですが、DB用途以外に使っていると、時々、嫌ぁ〜な問題にぶつかる事があります。

一例として、ソートの問題があります。


DataTable.Rowsのインデクサは読み取り専用のため、

t.Rows[i] = t.Rows[i + 1];

みたいなことが出来ず、ソートを掛ける場合は、ソート済みデータを読み直すか、DataViewを使うことになります。大量のデータを入れなおしたくはないので、通常はDataViewを使うと思います。

t.DefaultView.Sort = "Key1, Key2, Key3";

とか。

さて、ここで一度、DataTableから離れて、単純な文字列のソートを見てみます。

       static void TestList()
        {
            List<string> list = new List<string>();

            list.Add("a");
            list.Add("A");
            list.Add("_");

            list.Sort();

            Console.WriteLine("default sort.");
            foreach (var item in list)
                Console.WriteLine(item);

            // ASCIIコード順に並んでほしい
            Console.WriteLine("ordinal sort.");
            list.Sort(StringComparer.Ordinal);
            foreach (var item in list)
                Console.WriteLine(item);
        }

/* 結果
default sort.
_
a
A
ordinal sort.
A
_
a
 */

文字列を何も考えずにソートするとC言語などのソート(ASCIIコード順)とはならず、それでは困る場面もあったりします。(Oracleでorder byを使った場合の並び順もデフォルトはbinaryなので結果が変わる)。Listの場合は、比較用インタフェースを渡せるのでたいした問題はありません。

話をDataTableに戻します。

t.DefaultView.Sort = "Key1, Key2, Key3";

これを見る限り、文字列の比較方法を変更する手段がありそうに見えません。となると、MyStringのようなクラスを作成して、IComparableなどを実装するのでしょうか?正攻法かも知れませんが、文字列でそれをやるのはぞっとします。(^^;

色々考えましたが、ソート用のカラムを追加してごにょごにょするのが手っ取り早い気がします。たとえば、こんな感じ。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Collections;

namespace SortSample
{
    class Program
    {
        static void TestDataTable()
        {
            DataTable t = new DataTable();
            t.Columns.Add("Key1", typeof(int));
            t.Columns.Add("Key2", typeof(string));
            t.Columns.Add("Key3", typeof(string));

            // 英大文字、小文字、_を含むデータを生成する
            for (int i = 30; i > 0; --i)
            {
                var row = t.NewRow();
                row["Key1"] = i % 2;
                row["Key2"] = (char)(i % 3 == 0 ? '_' : i % 2 == 0 ? ('A' + i % 3) : ('a' + i % 3));
                row["Key3"] = (char)(i % 12 == 0 ? '_' : i % 2 == 0 ? ('A' + i % 12) : ('a' + i % 12));
                t.Rows.Add(row);
            }

            t.AcceptChanges();

            // 文字列型の列はASCIIコード順で並べたいが、比較方法を指定することが・・・
            // t.DefaultView.Sort = "Key1, Key2, Key3";

            // 自作ソートを呼ぶ
            Sort(t, "Key1, Key2, Key3");

            // t.DefaultView経由でアクセスすると指定どおりに並んでいる
            var view = t.DefaultView;
        }

        static void Swap<T>(ref T left, ref T right)
        {
            T row = left;
            left = right;
            right = row;
        }

        static void Sort(DataTable t, string sort)
        {
            // ソート用の仮想キー
            const string DUMMY = "_NO_";

            // ソート用のダミーカラム
            if (!t.Columns.Contains(DUMMY))
                t.Columns.Add(DUMMY, typeof(int));

            // 安定ソートを使うのでキーの逆順でソートを掛ける
            var keys = sort.Split(',').Select(s => s.Trim()).Reverse();

            foreach (string key in keys)
            {
                // 直前のソート状態を利用するので毎回DataViewからarrayを作成する
                DataRow array = t.DefaultView.OfType<DataRowView>().Select(view => view.Row).ToArray();

                var index = t.Columns.IndexOf(key);
                var column = t.Columns[index];
                if (column.DataType == typeof(string))
                    BubbleSort(array, (x, y)=>StringComparer.Ordinal.Compare(x[index], y[index]));
                else
                    BubbleSort(array, (x, y)=>Comparer.Default.Compare(x[index], y[index]));

                for (int i = 0; i < array.Length; ++i)
                {
                    // 元の状態を保存
                    var state = array[i].RowState;
                    array[i][DUMMY] = i;
                    // 元が変更無しなら、状態を戻す(addやdeleted状態はとりあえず無視)
                    if (state == DataRowState.Unchanged)
                        array[i].AcceptChanges();
                }

                t.DefaultView.Sort = DUMMY;
            }
        }

        static void BubbleSort<DataRow>(DataRow array, Func<DataRow, DataRow, int> compare)
        {
            for (int i = 0; i < array.Length - 1; ++i)
            {
                for (int j = array.Length - 1; j > i; --j)
                {
                    if (compare(array[j - 1], array[j]) > 0)
                        Swap(ref array[j - 1], ref array[j]);
                }
            }
        }

        static void Main(string[] args)
        {
            TestDataTable();
        }
    }
}

ミソは安定ソートを使っているところでしょうか。実装が面倒なのでとりあえず、バブルソートで書いていますが、本番ではもう少し早い安定ソートに置き換えたほうが良いかも知れません。