JavaScript Tips | 配列ユーティリティ:多段ソート

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「多段ソート」

ここでの「多段ソート」は、複数のキーを優先順位付きで使って並び替える処理です。
SQL でいう ORDER BY category ASC, price DESC のようなイメージです。

例えば、こんな並び順を作りたいとします。

まず「カテゴリ」で昇順に。
同じカテゴリの中では「価格の安い順」に。
価格も同じなら「名前の昇順」に。

これを毎回 sort の比較関数で手書きすると、条件が増えるたびにカオスになります。
そこで、「多段ソートユーティリティ」として関数化しておくと、業務コードがかなりスッキリします。


まずはイメージ:単一キーソートとの違い

単一キーソートとの比較

単一キーソートは「このキーだけで並び替える」ものです。

// 価格の昇順
sortBy(items, (item) => item.price, "asc");
JavaScript

多段ソートは、「第1キーで比較して、同じだったら第2キー、それでも同じなら第3キー…」というふうに、
優先順位付きで比較を重ねていくものです。

1段目: category 昇順
2段目: price 昇順
3段目: name 昇順

比較関数の中で「順番に条件を試していき、差がついたところで決着をつける」というイメージを持ってください。


基本形:単純な多段ソートの比較関数

まずはベタ書きで理解する

例として、次のような商品配列を考えます。

const products = [
  { id: 1, name: "A", category: "Food", price: 500 },
  { id: 2, name: "B", category: "Book", price: 1200 },
  { id: 3, name: "C", category: "Food", price: 300 },
  { id: 4, name: "D", category: "Book", price: 800 },
];
JavaScript

これを「カテゴリ昇順 → 価格昇順」で並び替えたいとします。
まずは sort の比較関数をベタ書きしてみます。

const copy = products.slice();

copy.sort((a, b) => {
  if (a.category < b.category) return -1;
  if (a.category > b.category) return 1;

  if (a.price < b.price) return -1;
  if (a.price > b.price) return 1;

  return 0;
});
JavaScript

ここでやっていることはこうです。

1段目として category を比較する。
違えば、その結果で並び順が決まる。
同じだった場合だけ、2段目として price を比較する。
それでも同じなら「同じ」とみなして 0 を返す。

この「差がつくまで順番に比較していく」という流れが、多段ソートの本質です。


汎用化の第一歩:単一キー比較関数を組み合わせる

「1キー分の比較」を小さな関数にする

多段ソートをきれいに書くコツは、「1キー分の比較」を小さな関数に切り出すことです。

例えば、数値キーの昇順比較関数をこう定義します。

function compareNumberAsc(a, b, key) {
  const va = a?.[key];
  const vb = b?.[key];

  if (typeof va !== "number" || typeof vb !== "number") {
    return 0;
  }

  if (va < vb) return -1;
  if (va > vb) return 1;
  return 0;
}
JavaScript

文字列キーの昇順比較関数はこうです。

function compareStringAsc(a, b, key) {
  const va = a?.[key];
  const vb = b?.[key];

  if (typeof va !== "string" || typeof vb !== "string") {
    return 0;
  }

  return va.localeCompare(vb);
}
JavaScript

これを使って、「カテゴリ昇順 → 価格昇順」の多段ソートを書くとこうなります。

const copy = products.slice();

copy.sort((a, b) => {
  const c1 = compareStringAsc(a, b, "category");
  if (c1 !== 0) return c1;

  const c2 = compareNumberAsc(a, b, "price");
  if (c2 !== 0) return c2;

  return 0;
});
JavaScript

「1キー分の比較結果を見て、0 でなければそのまま返す。0 なら次のキーへ。」
というパターンが見えてきたら、もう一歩で汎用化できます。


本命:条件配列を渡す汎用多段ソート sortByMulti

ソート条件を「配列」で渡す

多段ソートをユーティリティにするときは、
「どのキーを、どの順番で、昇順か降順か」を配列で渡せるようにするのが定番です。

例えば、こんな感じの指定をしたいとします。

const criteria = [
  { key: "category", type: "string", order: "asc" },
  { key: "price", type: "number", order: "asc" },
];
JavaScript

これを受け取ってソートする関数を作ります。

function sortByMulti(array, criteria) {
  if (!Array.isArray(array)) {
    return [];
  }

  const copy = array.slice();

  copy.sort((a, b) => {
    for (const c of criteria) {
      const { key, type = "string", order = "asc" } = c;
      const factor = order === "desc" ? -1 : 1;

      const va = a?.[key];
      const vb = b?.[key];

      let result = 0;

      if (type === "number") {
        const na = typeof va === "number" ? va : Number.NaN;
        const nb = typeof vb === "number" ? vb : Number.NaN;

        if (Number.isNaN(na) && Number.isNaN(nb)) {
          result = 0;
        } else if (Number.isNaN(na)) {
          result = 1;
        } else if (Number.isNaN(nb)) {
          result = -1;
        } else if (na < nb) {
          result = -1;
        } else if (na > nb) {
          result = 1;
        } else {
          result = 0;
        }
      } else {
        const sa = va == null ? "" : String(va);
        const sb = vb == null ? "" : String(vb);
        result = sa.localeCompare(sb);
      }

      if (result !== 0) {
        return result * factor;
      }
    }

    return 0;
  });

  return copy;
}
JavaScript

重要なポイントを深掘りする

比較関数の中でやっていることは、次の繰り返しです。

  1. criteria の配列を先頭から順に見る。
  2. 各条件ごとに、a と b の値を取り出して比較する。
  3. 差がついたら、その結果に factor(昇順なら 1、降順なら -1)を掛けて返す。
  4. 差がつかなければ、次の条件に進む。
  5. 最後まで差がつかなければ 0 を返す。

この「for で条件を回しながら、最初に差がついたところで return する」という構造が、多段ソートの核です。

数値と文字列で比較方法を分けているのは、「数値は大小比較」「文字列は localeCompare」という基本ルールを守るためです。

NaNnull などの不正値は、「後ろに寄せる」ようにしています。
これにより、「まともなデータ」が先に並び、「おかしいデータ」が後ろに固まるので、実務上扱いやすくなります。


実際の使用例でイメージを固める

例1:カテゴリ昇順 → 価格昇順

const products = [
  { id: 1, name: "A", category: "Food", price: 500 },
  { id: 2, name: "B", category: "Book", price: 1200 },
  { id: 3, name: "C", category: "Food", price: 300 },
  { id: 4, name: "D", category: "Book", price: 800 },
];

const sorted = sortByMulti(products, [
  { key: "category", type: "string", order: "asc" },
  { key: "price", type: "number", order: "asc" },
]);
JavaScript

並び順はこうなります。

[
  { id: 2, name: "B", category: "Book", price: 1200 },
  { id: 4, name: "D", category: "Book", price: 800 },
  { id: 3, name: "C", category: "Food", price: 300 },
  { id: 1, name: "A", category: "Food", price: 500 },
]
JavaScript

まず category"Book""Food" の順に並び、
同じカテゴリ内では price の昇順になっています。

例2:部署昇順 → 年齢降順 → 名前昇順

const employees = [
  { name: "Alice", dept: "Sales", age: 30 },
  { name: "Bob", dept: "Sales", age: 25 },
  { name: "Charlie", dept: "Dev", age: 35 },
  { name: "Diana", dept: "Dev", age: 35 },
];

const sortedEmployees = sortByMulti(employees, [
  { key: "dept", type: "string", order: "asc" },
  { key: "age", type: "number", order: "desc" },
  { key: "name", type: "string", order: "asc" },
]);
JavaScript

並び順はこうなります。

[
  { name: "Charlie", dept: "Dev", age: 35 },
  { name: "Diana", dept: "Dev", age: 35 },
  { name: "Alice", dept: "Sales", age: 30 },
  { name: "Bob", dept: "Sales", age: 25 },
]
JavaScript

dept"Dev""Sales" の順。
同じ部署内では age の降順。
年齢も同じなら name の昇順。

という多段ソートが、条件配列を見れば一目で分かる形で書けています。


実務で意識してほしい設計のポイント

「条件をデータで表現する」ことの強さ

多段ソートをユーティリティにするときのキモは、
「ソート条件をコードではなく“データ”として表現する」ことです。

[
  { key: "dept", type: "string", order: "asc" },
  { key: "age", type: "number", order: "desc" },
  { key: "name", type: "string", order: "asc" },
]
JavaScript

この配列を見れば、「どういう優先順位で、どのキーを、どの向きでソートしているか」が一目で分かります。
条件が増えても、「配列に1行足すだけ」で済みます。

ビューや API のパラメータからこの条件配列を組み立てるようにすれば、
「画面からソート条件を切り替える」ような機能も簡単に実装できます。

「元の配列を壊さない」ことをデフォルトにする

sort は破壊的なので、ユーティリティは必ず slice() でコピーしてからソートする設計にしておくと安全です。
「どうしても破壊的にソートしたい」場面があるなら、sortByMultiInPlace のような別関数に分けると、意図が明確になります。

「不正値の扱い」を決めておく

キーが存在しない、nullNaN、型が違う――現実のデータではよくあります。
多段ソートでは、ここを放置すると結果が読めなくなります。

今回の実装では、ざっくりこう決めています。

数値キーで数値でないもの → NaN として扱い、後ろに寄せる。
文字列キーで null/undefined → 空文字として扱う。

このルールをユーティリティ側で固定しておくと、
呼び出し側は「変なデータは後ろに行く」と理解しておけばよくなります。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

const products = [
  { id: 1, name: "A", category: "Food", price: 500 },
  { id: 2, name: "B", category: "Book", price: 1200 },
  { id: 3, name: "C", category: "Food", price: 300 },
  { id: 4, name: "D", category: "Book", price: 800 },
];

sortByMulti(products, [
  { key: "category", type: "string", order: "asc" },
  { key: "price", type: "number", order: "asc" },
]);

const employees = [
  { name: "Alice", dept: "Sales", age: 30 },
  { name: "Bob", dept: "Sales", age: 25 },
  { name: "Charlie", dept: "Dev", age: 35 },
  { name: "Diana", dept: "Dev", age: 35 },
];

sortByMulti(employees, [
  { key: "dept", type: "string", order: "asc" },
  { key: "age", type: "number", order: "desc" },
  { key: "name", type: "string", order: "asc" },
]);
JavaScript

「どの条件で、どの順番に並び替えられているか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function sortByMulti(...) { ... }
JavaScript

のような関数を置き、

「複数条件でソートしたくなったら、必ずこの“多段ソートユーティリティ”を通す」

というルールを作ってみてください。
そうすると、あなたのソート処理は、「if だらけの比較関数」から、「読みやすくて変更に強い業務レベルの実装」に一段ステップアップします。

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