何をしたいユーティリティか:「多段ソート」
ここでの「多段ソート」は、複数のキーを優先順位付きで使って並び替える処理です。
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重要なポイントを深掘りする
比較関数の中でやっていることは、次の繰り返しです。
- criteria の配列を先頭から順に見る。
- 各条件ごとに、a と b の値を取り出して比較する。
- 差がついたら、その結果に
factor(昇順なら 1、降順なら -1)を掛けて返す。 - 差がつかなければ、次の条件に進む。
- 最後まで差がつかなければ 0 を返す。
この「for で条件を回しながら、最初に差がついたところで return する」という構造が、多段ソートの核です。
数値と文字列で比較方法を分けているのは、「数値は大小比較」「文字列は localeCompare」という基本ルールを守るためです。
NaN や null などの不正値は、「後ろに寄せる」ようにしています。
これにより、「まともなデータ」が先に並び、「おかしいデータ」が後ろに固まるので、実務上扱いやすくなります。
実際の使用例でイメージを固める
例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 },
]
JavaScriptdept で "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 のような別関数に分けると、意図が明確になります。
「不正値の扱い」を決めておく
キーが存在しない、null、NaN、型が違う――現実のデータではよくあります。
多段ソートでは、ここを放置すると結果が読めなくなります。
今回の実装では、ざっくりこう決めています。
数値キーで数値でないもの → 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 だらけの比較関数」から、「読みやすくて変更に強い業務レベルの実装」に一段ステップアップします。
