JavaScript Tips | 配列ユーティリティ:キー指定ソート

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「キー指定ソート」

ここでの「キー指定ソート」は、オブジェクトの配列を「特定のキー(プロパティ)」を基準に並び替える処理です。
SQL の ORDER BY price ASCORDER BY created_at DESC を、JavaScript の配列でやるイメージです。

例えば、次のようなことをしたくなります。

商品一覧を「価格の安い順」「高い順」に並べたい。
ユーザー一覧を「名前の昇順」「登録日の降順」に並べたい。
ログを「タイムスタンプの昇順」に並べたい。

毎回 array.sort((a, b) => …) を手書きすると、条件が増えるたびにぐちゃっとしてきます。
そこで、「キー指定ソートユーティリティ」として関数を用意しておくと、業務コードがかなり読みやすくなります。


基本の考え方:sort と比較関数

sort の「比較関数」が何をしているか

JavaScript の Array.prototype.sort は、「比較関数」を渡してあげると、そのルールに従って並び替えてくれる関数です。

比較関数は、ざっくりこういうルールで動きます。

compare(a, b) が 0 より小さい → a が先、b が後。
compare(a, b) が 0 より大きい → b が先、a が後。
compare(a, b) が 0 → 順序は変えない(同じとみなす)。

数値の昇順なら、こう書けます。

numbers.sort((a, b) => a - b);
JavaScript

a - b が負なら a が小さい → a が先。
a - b が正なら b が小さい → b が先。

この「比較関数の仕組み」を理解しておくと、キー指定ソートも怖くなくなります。


一番基本:数値キーで昇順ソート

key を指定して昇順に並べるユーティリティ

まずは、「オブジェクト配列を、指定した数値キーで昇順に並べる」関数です。

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

  const copy = array.slice();

  copy.sort((a, b) => {
    const va = a?.[key];
    const vb = b?.[key];

    const na = typeof va === "number" ? va : Number.NaN;
    const nb = typeof vb === "number" ? vb : Number.NaN;

    if (Number.isNaN(na) && Number.isNaN(nb)) return 0;
    if (Number.isNaN(na)) return 1;
    if (Number.isNaN(nb)) return -1;

    return na - nb;
  });

  return copy;
}
JavaScript

重要なポイントをかみ砕いて説明する

最初に array.slice() でコピーを作っています。
sort は配列を「破壊的に」並び替えるので、元の配列を壊したくない場合は必ずコピーしてからソートします。

比較関数の中では、まずキーの値を取り出しています。

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

?. はオプショナルチェーンで、「a が null/undefined でもエラーにしないで undefined を返す」書き方です。

次に、「数値として扱えるか」をチェックしています。

const na = typeof va === "number" ? va : Number.NaN;
const nb = typeof vb === "number" ? vb : Number.NaN;
JavaScript

数値でないものは NaN にしてしまい、そのあとで「NaN は一番後ろに追いやる」というルールにしています。

if (Number.isNaN(na) && Number.isNaN(nb)) return 0;
if (Number.isNaN(na)) return 1;
if (Number.isNaN(nb)) return -1;
JavaScript

最後に、普通に na - nb で昇順ソートします。

return na - nb;
JavaScript

実際の動き

const products = [
  { id: 1, name: "A", price: 1000 },
  { id: 2, name: "B", price: 500 },
  { id: 3, name: "C", price: 1500 },
];

const sorted = sortByNumberKey(products, "price");
/*
[
  { id: 2, name: "B", price: 500 },
  { id: 1, name: "A", price: 1000 },
  { id: 3, name: "C", price: 1500 },
]
*/
JavaScript

昇順・降順を切り替えられるようにする

order を指定できる sortByNumberKey

昇順だけだと足りないので、「昇順 / 降順」を切り替えられるようにします。

function sortByNumberKeyWithOrder(array, key, order = "asc") {
  if (!Array.isArray(array)) {
    return [];
  }

  const copy = array.slice();

  const factor = order === "desc" ? -1 : 1;

  copy.sort((a, b) => {
    const va = a?.[key];
    const vb = b?.[key];

    const na = typeof va === "number" ? va : Number.NaN;
    const nb = typeof vb === "number" ? vb : Number.NaN;

    if (Number.isNaN(na) && Number.isNaN(nb)) return 0;
    if (Number.isNaN(na)) return 1;
    if (Number.isNaN(nb)) return -1;

    return (na - nb) * factor;
  });

  return copy;
}
JavaScript

factor がミソです。

昇順なら factor = 1na - nb のまま。
降順なら factor = -1na - nb の符号が反転して、逆順になる。

比較関数の中身を 2 回書き分けるのではなく、「最後に掛ける係数だけ変える」という発想です。

実際の動き

sortByNumberKeyWithOrder(products, "price", "asc");
// 価格の安い順

sortByNumberKeyWithOrder(products, "price", "desc");
// 価格の高い順
JavaScript

文字列キーでソートする(localeCompare を使う)

名前やタイトルを「あいうえお順」「アルファベット順」に

文字列をソートするときに、単純に a.name > b.name で比較するのはあまりおすすめしません。
言語や大文字小文字の扱いなどを考えると、String.prototype.localeCompare を使うのが安全です。

function sortByStringKey(array, key, order = "asc") {
  if (!Array.isArray(array)) {
    return [];
  }

  const copy = array.slice();
  const factor = order === "desc" ? -1 : 1;

  copy.sort((a, b) => {
    const va = a?.[key];
    const vb = b?.[key];

    const sa = typeof va === "string" ? va : "";
    const sb = typeof vb === "string" ? vb : "";

    return sa.localeCompare(sb) * factor;
  });

  return copy;
}
JavaScript

localeCompare は、「文字列同士をその言語環境に応じたルールで比較して、負・0・正を返す」メソッドです。
これを factor で反転させることで、昇順・降順を切り替えています。

実際の動き

const users = [
  { id: 1, name: "Tanaka" },
  { id: 2, name: "Sato" },
  { id: 3, name: "Yamada" },
];

sortByStringKey(users, "name", "asc");
// name の昇順

sortByStringKey(users, "name", "desc");
// name の降順
JavaScript

汎用版:任意の「キー関数」でソートする sortBy

「キー名」では足りない場面

業務では、「単純なプロパティ」だけでなく、
「計算した値」でソートしたいことがよくあります。

例えば、「単価 × 数量の金額でソートしたい」「日付文字列を Date に変換してソートしたい」などです。

そのときは、「要素からソート用のキーを取り出す関数」を渡せるようにします。

function sortBy(array, keyFn, order = "asc") {
  if (!Array.isArray(array)) {
    return [];
  }

  const copy = array.slice();
  const factor = order === "desc" ? -1 : 1;

  copy.sort((a, b) => {
    const ka = keyFn(a);
    const kb = keyFn(b);

    if (ka == null && kb == null) return 0;
    if (ka == null) return 1;
    if (kb == null) return -1;

    if (typeof ka === "number" && typeof kb === "number") {
      if (Number.isNaN(ka) && Number.isNaN(kb)) return 0;
      if (Number.isNaN(ka)) return 1;
      if (Number.isNaN(kb)) return -1;
      return (ka - kb) * factor;
    }

    const sa = String(ka);
    const sb = String(kb);
    return sa.localeCompare(sb) * factor;
  });

  return copy;
}
JavaScript

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

keyFn は、「要素を受け取って、その要素の“ソートキー”を返す関数」です。
このキーが小さいものから順に並ぶようにソートします。

比較の流れはこうです。

両方 null / undefined → 同じとみなす。
片方だけ null / undefined → そちらを後ろに回す。
両方数値 → 数値として比較(NaN は後ろ)。
それ以外 → 文字列に変換して localeCompare

これで、「数値キーでも文字列キーでも、だいたいそれっぽく動く汎用ソート」ができます。

実際の使い方

金額(price × quantity)の昇順でソートする例です。

const items = [
  { id: 1, price: 1000, quantity: 2 }, // 2000
  { id: 2, price: 500, quantity: 3 },  // 1500
  { id: 3, price: 2000, quantity: 1 }, // 2000
];

const sorted = sortBy(items, (item) => item.price * item.quantity, "asc");
/*
[
  { id: 2, price: 500, quantity: 3 },  // 1500
  { id: 1, price: 1000, quantity: 2 }, // 2000
  { id: 3, price: 2000, quantity: 1 }, // 2000
]
*/
JavaScript

日付文字列を Date に変換してソートする例です。

const logs = [
  { id: 1, at: "2024-01-10T12:00:00Z" },
  { id: 2, at: "2023-12-31T23:59:59Z" },
  { id: 3, at: "2024-02-01T09:00:00Z" },
];

const sortedLogs = sortBy(
  logs,
  (log) => new Date(log.at).getTime(),
  "asc"
);
JavaScript

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

「元の配列を壊すかどうか」をはっきり決める

sort は元の配列を書き換えます。
業務コードでは、「元データはそのまま」「表示用だけ並び替えたい」ことが多いので、
ユーティリティは基本的に「コピーしてからソートする」設計にしておくのがおすすめです。

もし「パフォーマンス優先で、あえて破壊的にソートしたい」場面があるなら、
sortByInPlace のような別関数にして、名前で意図を伝えるとよいです。

「キーが存在しない」「値が不正」のときのルール

現実のデータでは、キーがなかったり、null だったり、文字列だったりします。
ここを放置すると、ソート結果が読めなくなります。

ユーティリティ側で、例えば次のように決めておくと安定します。

キーがない・null・undefined → 一番後ろに寄せる。
数値ソートでは、数値でないものは NaN として扱い、後ろに寄せる。

こうしておくと、「まともな値を持っているもの」が先に並び、
「おかしなデータ」は後ろに固まるので、デバッグもしやすくなります。

「昇順 / 降順」を文字列で指定する

order"asc" / "desc" の文字列で指定するのは、
コードを読む人にとって非常に分かりやすいです。

true / false1 / -1 で指定するよりも、
「これは昇順か降順か」を一瞬で理解できます。

ユーティリティのインターフェースは、「呼び出す側の読みやすさ」を最優先で設計すると、あとで効いてきます。


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

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

const products = [
  { id: 1, name: "A", price: 1000 },
  { id: 2, name: "B", price: 500 },
  { id: 3, name: "C", price: 1500 },
];

sortByNumberKeyWithOrder(products, "price", "asc");
sortByNumberKeyWithOrder(products, "price", "desc");

const users = [
  { id: 1, name: "Tanaka" },
  { id: 2, name: "Sato" },
  { id: 3, name: "Yamada" },
];

sortByStringKey(users, "name", "asc");
sortByStringKey(users, "name", "desc");

const items = [
  { id: 1, price: 1000, quantity: 2 },
  { id: 2, price: 500, quantity: 3 },
  { id: 3, price: 2000, quantity: 1 },
];

sortBy(items, (item) => item.price * item.quantity, "asc");
JavaScript

「どのキーで並び替えているか」「昇順と降順でどう変わるか」「元の配列が壊れていないか」を、自分の目で確認してみてください。

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

export function sortByNumberKeyWithOrder(...) { ... }
export function sortByStringKey(...) { ... }
export function sortBy(...) { ... }
JavaScript

のような関数を置き、

「配列をキーで並び替えたくなったら、必ずこの“キー指定ソートユーティリティ”を通す」

というルールを作ってみてください。
それだけで、あなたのソート処理は、「その場しのぎの sort」から、「意図と一貫性を備えた業務レベルの実装」に変わっていきます。

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