C# Tips | コレクション・LINQ:カスタムソート

C# C#
スポンサーリンク

はじめに:「カスタムソート」は“業務ルールをそのまま並び順にする技”

「優先度は High → Middle → Low の順」
「ステータスは 未処理 → 処理中 → 完了 の順」
「部署コードは A, B, C の順ではなく、営業 → 開発 → 管理 の順」

こういう“人間が決めた順番”って、数値の昇順・降順では表現できません。
この「業務独自の順番」をコードに落とし込むのが、カスタムソート(カスタム並び替え)です。

ここでは、初心者でも書けるように

  • 「優先度を High → Middle → Low の順に並べる」
  • 「ステータスを任意の順番で並べる」
  • 比較ロジックをユーティリティ化して再利用する

という流れで、かみ砕いて説明していきます。


基本アイデア:「並び順を数値に変換してからソートする」

発想:業務の順番を「ランク(順位)」にしてしまう

例えば、優先度を表す文字列 "High", "Middle", "Low" があるとします。
これを「High → Middle → Low の順に並べたい」とき、
そのまま文字列の昇順・降順ではうまくいきません。

そこでやることは一つです。

「High を 1、Middle を 2、Low を 3 のように“ランク(順位)”に変換してから、その数値でソートする」

これがカスタムソートの基本パターンです。


例1:優先度 High → Middle → Low の順に並べる

データモデルを用意する

まずは簡単なクラスを用意します。

public class TaskItem
{
    public string Title { get; set; } = "";
    public string Priority { get; set; } = ""; // "High", "Middle", "Low"
}
C#

データを作ります。

var tasks = new List<TaskItem>
{
    new TaskItem { Title = "A", Priority = "Low" },
    new TaskItem { Title = "B", Priority = "High" },
    new TaskItem { Title = "C", Priority = "Middle" },
    new TaskItem { Title = "D", Priority = "High" },
};
C#

優先度を「ランク」に変換する関数を書く

ここが一番大事なところです。
業務ルールを「数値に変換する関数」として切り出します。

public static int PriorityRank(string priority)
{
    return priority switch
    {
        "High"   => 1,
        "Middle" => 2,
        "Low"    => 3,
        _        => 99 // 想定外は一番後ろに
    };
}
C#

ここでの重要ポイントは、「業務の順番を“数字の小さいほうが先”という形にマッピングしている」ことです。
High が一番優先度が高いので 1、Middle が 2、Low が 3。
想定外の値は 99 にして、最後尾に追いやるようにしています。

ランクをキーにしてソートする

あとは、このランクを OrderBy のキーに使うだけです。

var sorted = tasks
    .OrderBy(t => PriorityRank(t.Priority));

foreach (var t in sorted)
{
    Console.WriteLine($"{t.Priority}: {t.Title}");
    // High: B
    // High: D
    // Middle: C
    // Low: A
}
C#

これで、「High → Middle → Low」の順番が実現できました。


例2:ステータスを「任意の順番」で並べる

ステータスの順番を配列で定義する

今度は、ステータス "New", "InProgress", "Done"
「New → InProgress → Done」の順に並べたいとします。

まず、「正解の順番」を配列で定義します。

string[] statusOrder = { "New", "InProgress", "Done" };
C#

次に、「ステータス文字列を、その配列のインデックスに変換する」関数を作ります。

public static int StatusRank(string status, string[] order)
{
    int index = Array.IndexOf(order, status);
    return index >= 0 ? index : int.MaxValue; // 見つからなければ最後尾
}
C#

ここでの重要ポイントは、「配列の位置=並び順」という考え方です。
Array.IndexOf は、「その値が配列の何番目にあるか」を返してくれます。

ステータス順にソートする

データを用意します。

public class Ticket
{
    public string Title { get; set; } = "";
    public string Status { get; set; } = ""; // "New", "InProgress", "Done"
}

var tickets = new List<Ticket>
{
    new Ticket { Title = "A", Status = "Done" },
    new Ticket { Title = "B", Status = "New" },
    new Ticket { Title = "C", Status = "InProgress" },
    new Ticket { Title = "D", Status = "New" },
};
C#

ソートします。

var sorted = tickets
    .OrderBy(t => StatusRank(t.Status, statusOrder));

foreach (var t in sorted)
{
    Console.WriteLine($"{t.Status}: {t.Title}");
    // New: B
    // New: D
    // InProgress: C
    // Done: A
}
C#

ここでの重要ポイントは、「業務側で“正解の順番”を配列として持ち、それをソートキーに変換している」ことです。
順番を変えたくなったら、statusOrder の中身を変えるだけで済みます。


例3:複数条件のカスタムソート(ステータス → 日付)

「ステータス順 → 同じステータス内では日付の新しい順」

さっきのステータスに加えて、「同じステータスの中では日付の新しい順」にしたいとします。

public class Ticket
{
    public string Title { get; set; } = "";
    public string Status { get; set; } = "";
    public DateTime CreatedAt { get; set; }
}
C#

データを用意します。

var tickets = new List<Ticket>
{
    new Ticket { Title = "A", Status = "Done",       CreatedAt = new DateTime(2026, 2, 10) },
    new Ticket { Title = "B", Status = "New",        CreatedAt = new DateTime(2026, 2, 12) },
    new Ticket { Title = "C", Status = "InProgress", CreatedAt = new DateTime(2026, 2, 11) },
    new Ticket { Title = "D", Status = "New",        CreatedAt = new DateTime(2026, 2, 13) },
};
C#

ソートはこう書けます。

var sorted = tickets
    .OrderBy(t => StatusRank(t.Status, statusOrder))   // ステータス順
    .ThenByDescending(t => t.CreatedAt);               // 同じステータス内では新しい順
C#

ここでの重要ポイントは、「カスタムキー(StatusRank)と普通のキー(CreatedAt)を組み合わせている」ことです。
OrderBy で“業務ルールの順番”を決め、ThenBy / ThenByDescending で“自然な順番(昇順・降順)”を足していくイメージです。


比較ロジックをユーティリティ化する

「ランク関数」を共通化しておく

同じ優先度やステータスの順番を、あちこちで使うなら、
ランク関数をユーティリティクラスにまとめておくと便利です。

public static class SortOrder
{
    private static readonly string[] StatusOrder = { "New", "InProgress", "Done" };

    public static int StatusRank(string status)
    {
        int index = Array.IndexOf(StatusOrder, status);
        return index >= 0 ? index : int.MaxValue;
    }

    public static int PriorityRank(string priority)
    {
        return priority switch
        {
            "High"   => 1,
            "Middle" => 2,
            "Low"    => 3,
            _        => 99
        };
    }
}
C#

使う側は、こう書くだけになります。

var sortedTickets = tickets
    .OrderBy(t => SortOrder.StatusRank(t.Status));

var sortedTasks = tasks
    .OrderBy(t => SortOrder.PriorityRank(t.Priority));
C#

ここでの重要ポイントは、「業務ルールを“名前のついた関数”として外に出しておく」ことです。
これで、「この順番はどこで決まっているのか?」が一目で追えるようになります。


実務で意識してほしいこと

「何順か」を日本語で先に決める

カスタムソートを書く前に、必ず言葉で整理してください。

優先度は High → Middle → Low
ステータスは New → InProgress → Done
部署は 営業 → 開発 → 管理

これが決まれば、あとは「その順番を数値や配列のインデックスに変換する」だけです。

「魔法の if 文」ではなく「変換関数」として切り出す

ありがちな悪い例は、ソートの中に if 文をベタベタ書いてしまうことです。

// 読みにくい例
var sorted = tasks.OrderBy(t =>
{
    if (t.Priority == "High") return 1;
    if (t.Priority == "Middle") return 2;
    if (t.Priority == "Low") return 3;
    return 99;
});
C#

動きはしますが、テストしづらく、再利用もしにくいです。

これを、

public static int PriorityRank(string priority) { ... }
C#

という関数に切り出しておけば、
ソートの行は OrderBy(t => PriorityRank(t.Priority)) とシンプルになり、
ランク関数だけを個別にテストすることもできます。


まとめ:「カスタムソートユーティリティ」は“業務の順番をコードに刻む道具”

カスタムソートの本質は、

「業務で決まっている“人間の順番”を、
数値やインデックスに変換してからソートする」

という一点です。

押さえておきたいポイントは、

業務の順番をまず日本語で決める
その順番を「ランク(順位)」に変換する関数を作る
OrderBy(ランク関数) として使う
複数条件があるときは、OrderByThenBy / ThenByDescending を組み合わせる

ここまでできれば、「昇順・降順しか書けない」状態から抜け出して、
“業務ルールそのものを反映した並び順”を、C# と LINQ で自然に表現できるようになります。

タイトルとURLをコピーしました