T-CREATOR

Lodash の `orderBy` とマルチキーソート:昇降混在・自然順の完全攻略

Lodash の `orderBy` とマルチキーソート:昇降混在・自然順の完全攻略

JavaScript で配列をソートする際、単純な並び替えなら標準の sort メソッドで十分ですが、複数の条件を組み合わせたり、昇順と降順を混在させたりする場合は、コードが複雑になりがちです。そんな時に活躍するのが、Lodash の orderBy 関数です。

この記事では、Lodash の orderBy を使ったマルチキーソート、昇順・降順の混在、さらには「file1.txt」「file10.txt」を正しく並べる自然順ソートまで、実践的なテクニックを丁寧に解説します。複雑なデータ操作に悩んでいる方にとって、きっと役立つ内容になるでしょう。

背景

JavaScript 標準の sort メソッドの特徴

JavaScript には配列をソートするための Array.prototype.sort() メソッドが標準で用意されています。しかし、この標準メソッドには、実務で使う際にいくつかの課題があります。

javascript// 標準の sort メソッドの基本例
const numbers = [3, 1, 4, 1, 5, 9];
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 1, 3, 4, 5, 9]

この例のように、単純な数値のソートであれば問題ありませんが、複数の条件を組み合わせる場合、比較関数が複雑になってしまいます。

Lodash が提供する便利なソート機能

Lodash は、JavaScript のユーティリティライブラリとして、配列操作、オブジェクト操作、関数操作など、さまざまな便利な機能を提供しています。その中でも orderBy は、ソート処理を簡潔に記述できる強力な関数です。

javascript// Lodash のインポート
import _ from 'lodash';

// または特定の関数のみインポート
import { orderBy } from 'lodash';

以下の図は、JavaScript 標準の sort と Lodash の orderBy の関係性を示しています。

mermaidflowchart TB
    dev["開発者"] -->|シンプルなソート| native["Array.sort()"]
    dev -->|複雑なソート| lodash["Lodash orderBy()"]
    native -->|比較関数を実装| result1["ソート結果"]
    lodash -->|宣言的な記述| result2["ソート結果"]

    style native fill:#e1f5ff
    style lodash fill:#fff4e1
    style result1 fill:#f0f0f0
    style result2 fill:#f0f0f0

図で理解できる要点:

  • 標準の sort は比較関数を自分で実装する必要がある
  • Lodash の orderBy は宣言的に条件を指定できる
  • どちらも最終的にはソート結果を得られるが、記述方法が異なる

Lodash を使う理由

Lodash を使うことで、以下のようなメリットが得られます。

#メリット説明
1コードの可読性向上複雑な比較ロジックを書かずに、キーと順序を指定するだけ
2マルチキーソートが簡単複数の条件でのソートを配列で指定できる
3昇降混在が容易各キーに対して個別に昇順・降順を指定可能
4バグの減少標準的な実装パターンを使うことでバグが減る

課題

複数条件でのソートの複雑さ

実務では、単一の条件だけでソートすることは稀で、複数の条件を組み合わせる必要があります。例えば、ユーザーリストを「部署別、年齢順」にソートしたい場合を考えてみましょう。

標準の sort メソッドで実装すると、以下のように比較ロジックが複雑になってしまいます。

javascript// 標準 sort での複数条件ソート(複雑な例)
const users = [
  { name: '佐藤', department: '営業', age: 30 },
  { name: '田中', department: '開発', age: 25 },
  { name: '鈴木', department: '営業', age: 28 },
];

// 複雑な比較関数
users.sort((a, b) => {
  // まず部署で比較
  if (a.department < b.department) return -1;
  if (a.department > b.department) return 1;

  // 部署が同じなら年齢で比較
  return a.age - b.age;
});

この例では、たった 2 つの条件でも比較関数が 10 行近くになり、条件が増えるとさらに複雑になります。

昇順と降順の混在

もう一つの課題は、ある条件では昇順、別の条件では降順にしたい場合です。

javascript// 部署は昇順、年齢は降順にしたい場合
users.sort((a, b) => {
  // 部署で比較(昇順)
  if (a.department < b.department) return -1;
  if (a.department > b.department) return 1;

  // 年齢で比較(降順)- 符号を反転
  return b.age - a.age;
});

昇順と降順が混在すると、比較ロジックの符号を考える必要があり、可読性が下がってしまいます。

自然順ソートの難しさ

ファイル名や番号付き項目をソートする場合、「自然順(Natural Sort)」が必要になることがあります。自然順とは、人間が直感的に理解する順序のことです。

以下の表は、通常のソートと自然順ソートの違いを示しています。

#ファイル名通常のソート自然順ソート
1file1.txtfile1.txtfile1.txt
2file10.txtfile10.txtfile2.txt
3file2.txtfile2.txtfile10.txt
4file20.txtfile20.txtfile20.txt

通常のソートでは文字列として比較するため、「file10.txt」が「file2.txt」より前に来てしまいます。これは「1」が「2」より小さいと判断されるためです。

javascript// 通常のソートでは期待通りにならない例
const files = [
  'file2.txt',
  'file10.txt',
  'file1.txt',
  'file20.txt',
];
files.sort();
console.log(files);
// ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
// file10.txt が file2.txt より前に来てしまう

以下の図は、これらの課題がどのように発生するかを示しています。

mermaidstateDiagram-v2
    [*] --> SimpleSort: シンプルなソート要件
    [*] --> ComplexSort: 複雑なソート要件

    ComplexSort --> MultiKey: 複数キーでソート
    ComplexSort --> MixedOrder: 昇降混在
    ComplexSort --> NaturalSort: 自然順

    SimpleSort --> Success1: Array.sort() で解決
    MultiKey --> Problem1: 複雑な比較関数
    MixedOrder --> Problem2: 符号の制御が必要
    NaturalSort --> Problem3: 独自実装が必要

    Problem1 --> NeedSolution
    Problem2 --> NeedSolution
    Problem3 --> NeedSolution

    NeedSolution --> [*]: Lodash orderBy で解決
    Success1 --> [*]

図で理解できる要点:

  • シンプルなソートは標準メソッドで十分
  • 複雑なソートには「マルチキー」「昇降混在」「自然順」の 3 つの課題がある
  • これらの課題を解決するために Lodash の orderBy が有効

解決策

Lodash orderBy の基本構文

Lodash の orderBy 関数は、以下のシンプルな構文でソートを実行できます。

javascript// orderBy の基本構文
_.orderBy(collection, [iteratees], [orders]);

各パラメータの意味は以下の通りです。

#パラメータ説明
1collectionArray/Objectソート対象の配列またはオブジェクト
2iterateesArray/Function/Stringソートキーを指定(配列、関数、文字列)
3ordersArrayソート順序を指定('asc' または 'desc')

単一キーでのソート

まずは、最もシンプルな単一キーでのソート例から見ていきましょう。

javascript// Lodash のインポート
import { orderBy } from 'lodash';

// サンプルデータ
const users = [
  { name: '佐藤', age: 30 },
  { name: '田中', age: 25 },
  { name: '鈴木', age: 35 },
];

年齢で昇順にソートする場合は、以下のように記述します。

javascript// 年齢で昇順にソート
const sortedUsers = orderBy(users, ['age'], ['asc']);
console.log(sortedUsers);
// [
//   { name: '田中', age: 25 },
//   { name: '佐藤', age: 30 },
//   { name: '鈴木', age: 35 }
// ]

降順にしたい場合は、'desc' を指定するだけです。

javascript// 年齢で降順にソート
const sortedDesc = orderBy(users, ['age'], ['desc']);
console.log(sortedDesc);
// [
//   { name: '鈴木', age: 35 },
//   { name: '佐藤', age: 30 },
//   { name: '田中', age: 25 }
// ]

マルチキーソートの実装

複数のキーでソートする場合は、配列に複数の要素を指定します。

javascript// 複数条件のサンプルデータ
const employees = [
  {
    name: '佐藤',
    department: '営業',
    age: 30,
    salary: 5000000,
  },
  {
    name: '田中',
    department: '開発',
    age: 25,
    salary: 4500000,
  },
  {
    name: '鈴木',
    department: '営業',
    age: 28,
    salary: 4800000,
  },
  {
    name: '高橋',
    department: '開発',
    age: 30,
    salary: 5200000,
  },
  {
    name: '伊藤',
    department: '営業',
    age: 30,
    salary: 5100000,
  },
];

部署で昇順、その中で年齢でも昇順にソートする場合は、以下のように記述します。

javascript// 部署→年齢の順でソート(両方とも昇順)
const sorted = orderBy(
  employees,
  ['department', 'age'],
  ['asc', 'asc']
);

console.log(sorted);
// まず department でグループ化され、
// その中で age 順に並ぶ

この例では、最初に department でソートされ、同じ部署内で age によってソートされます。

昇降混在のソート

Lodash の orderBy の真価は、昇順と降順を簡単に混在させられる点にあります。

javascript// 部署は昇順、年齢は降順でソート
const mixedSort = orderBy(
  employees,
  ['department', 'age'],
  ['asc', 'desc'] // 部署は昇順、年齢は降順
);

console.log(mixedSort);
// department ごとにグループ化され、
// 各グループ内で age が大きい順に並ぶ

さらに複雑な例として、3 つのキーを使った場合を見てみましょう。

javascript// 部署(昇順)→年齢(降順)→給与(昇順)
const complexSort = orderBy(
  employees,
  ['department', 'age', 'salary'],
  ['asc', 'desc', 'asc']
);

console.log(complexSort);
// 1. department で昇順
// 2. 同じ department 内では age で降順
// 3. department と age が同じ場合は salary で昇順

以下の図は、マルチキーソートの処理フローを示しています。

mermaidflowchart LR
    data["元データ"] --> key1["第1キー<br/>でソート"]
    key1 --> key2["第2キー<br/>でソート"]
    key2 --> key3["第3キー<br/>でソート"]
    key3 --> result["ソート結果"]

    key1 -.->|同じ値| key2
    key2 -.->|同じ値| key3

    style data fill:#e1f5ff
    style result fill:#e1ffe1

図で理解できる要点:

  • ソートは指定したキーの順番に処理される
  • 前のキーで同じ値の場合にのみ、次のキーで比較される
  • 最終的に全ての条件を満たすソート結果が得られる

関数を使った柔軟なソート

キー名だけでなく、関数を使ってソート条件を指定することもできます。

javascript// 名前の文字数でソート
const sortedByNameLength = orderBy(
  employees,
  [(user) => user.name.length], // 関数でキーを計算
  ['asc']
);

console.log(sortedByNameLength);
// 名前が短い順に並ぶ

複数の関数を組み合わせることも可能です。

javascript// 部署名の文字数(昇順)→年齢(降順)
const functionalSort = orderBy(
  employees,
  [
    (emp) => emp.department.length, // 部署名の長さ
    'age', // 年齢(文字列指定も可)
  ],
  ['asc', 'desc']
);

console.log(functionalSort);

自然順ソートの実装

自然順ソートを実現するには、Lodash と組み合わせて使える natural-comparenatural-orderby といったライブラリを使う方法があります。ここでは、Lodash の関数機能を活用した実装方法をご紹介します。

まず、簡易的な自然順比較関数を作成します。

javascript// 自然順ソート用のキー抽出関数
function naturalSortKey(str) {
  // 数字部分と文字部分に分解
  return str.replace(/(\d+)/g, (match) => {
    // 数字部分を固定長の文字列に変換(ゼロパディング)
    return match.padStart(10, '0');
  });
}

この関数を orderBy と組み合わせて使います。

javascript// ファイル名のサンプルデータ
const files = [
  { name: 'file2.txt', size: 1024 },
  { name: 'file10.txt', size: 2048 },
  { name: 'file1.txt', size: 512 },
  { name: 'file20.txt', size: 4096 },
];

// 自然順でソート
const naturalSorted = orderBy(
  files,
  [(file) => naturalSortKey(file.name)],
  ['asc']
);

console.log(naturalSorted);
// [
//   { name: 'file1.txt', size: 512 },
//   { name: 'file2.txt', size: 1024 },
//   { name: 'file10.txt', size: 2048 },
//   { name: 'file20.txt', size: 4096 }
// ]

より高度な自然順ソートが必要な場合は、専用のライブラリを使うことをお勧めします。

javascript// natural-orderby ライブラリを使った例
// yarn add natural-orderby でインストール
import { orderBy as naturalOrderBy } from 'natural-orderby';

const files = ['file2.txt', 'file10.txt', 'file1.txt'];
const sorted = naturalOrderBy(files);
console.log(sorted);
// ['file1.txt', 'file2.txt', 'file10.txt']

具体例

実務で使える具体的なシナリオ

ここからは、実務でよく遭遇するシナリオを通して、Lodash の orderBy の活用方法を見ていきましょう。

シナリオ 1:ユーザー管理画面のソート

ユーザー管理画面で、複数の条件でユーザーリストをソートする例です。

javascript// ユーザーデータ
const users = [
  {
    id: 1,
    name: '佐藤太郎',
    status: 'active',
    lastLogin: '2025-01-15',
    points: 1200,
  },
  {
    id: 2,
    name: '田中花子',
    status: 'inactive',
    lastLogin: '2024-12-20',
    points: 800,
  },
  {
    id: 3,
    name: '鈴木一郎',
    status: 'active',
    lastLogin: '2025-01-18',
    points: 1500,
  },
  {
    id: 4,
    name: '高橋美咲',
    status: 'active',
    lastLogin: '2025-01-15',
    points: 1200,
  },
  {
    id: 5,
    name: '伊藤健太',
    status: 'suspended',
    lastLogin: '2024-11-10',
    points: 500,
  },
];

ステータス、最終ログイン日、ポイント数で複雑なソートを実行します。

javascriptimport { orderBy } from 'lodash';

// ステータス優先度を定義
const statusPriority = {
  active: 1,
  inactive: 2,
  suspended: 3,
};

// 複雑な条件でソート
const sortedUsers = orderBy(
  users,
  [
    // ステータス(カスタム優先度順)
    (user) => statusPriority[user.status],
    // 最終ログイン日(降順 = 新しい順)
    'lastLogin',
    // ポイント数(降順 = 多い順)
    'points',
  ],
  ['asc', 'desc', 'desc']
);

console.log(sortedUsers);

このコードでは、まず active ユーザーが表示され、その中で最終ログインが新しく、ポイントが多い順に並びます。

シナリオ 2:EC サイトの商品一覧

EC サイトで商品を複数の条件でソートする例です。

javascript// 商品データ
const products = [
  {
    id: 1,
    name: 'ノートPC',
    category: '電子機器',
    price: 120000,
    rating: 4.5,
    stock: 5,
  },
  {
    id: 2,
    name: 'マウス',
    category: '周辺機器',
    price: 2000,
    rating: 4.0,
    stock: 50,
  },
  {
    id: 3,
    name: 'キーボード',
    category: '周辺機器',
    price: 8000,
    rating: 4.5,
    stock: 0,
  },
  {
    id: 4,
    name: 'モニター',
    category: '電子機器',
    price: 30000,
    rating: 4.8,
    stock: 10,
  },
  {
    id: 5,
    name: 'USB ケーブル',
    category: '周辺機器',
    price: 500,
    rating: 3.5,
    stock: 100,
  },
];

在庫状況、評価、価格の順でソートします。

javascript// 在庫あり→評価高い→価格安い の順
const sortedProducts = orderBy(
  products,
  [
    // 在庫あり(stock > 0)を優先
    (product) => (product.stock > 0 ? 0 : 1),
    // 評価が高い順
    'rating',
    // 価格が安い順
    'price',
  ],
  ['asc', 'desc', 'asc']
);

console.log(sortedProducts);

在庫がある商品が優先的に表示され、その中で評価が高く、価格が安い順に並びます。

以下の図は、EC サイトでの商品ソートのロジックを示しています。

mermaidflowchart TD
    start["全商品"] --> check1{在庫あり?}
    check1 -->|Yes| group1["在庫ありグループ"]
    check1 -->|No| group2["在庫なしグループ"]

    group1 --> sort1["評価順<br/>でソート"]
    sort1 --> sort2["価格順<br/>でソート"]
    sort2 --> result1["表示優先"]

    group2 --> sort3["評価順<br/>でソート"]
    sort3 --> sort4["価格順<br/>でソート"]
    sort4 --> result2["表示後回し"]

    result1 --> display["画面表示"]
    result2 --> display

    style group1 fill:#e1ffe1
    style group2 fill:#ffe1e1
    style display fill:#e1f5ff

図で理解できる要点:

  • まず在庫の有無で大きく 2 つのグループに分ける
  • 各グループ内で評価、価格の順にソートする
  • 在庫ありグループが優先的に表示される

シナリオ 3:タスク管理アプリのタスクリスト

タスク管理アプリで、優先度、期限、ステータスでタスクをソートする例です。

javascript// タスクデータ
const tasks = [
  {
    id: 1,
    title: 'プレゼン資料作成',
    priority: 'high',
    dueDate: '2025-01-25',
    status: 'in_progress',
  },
  {
    id: 2,
    title: 'メール返信',
    priority: 'low',
    dueDate: '2025-01-22',
    status: 'todo',
  },
  {
    id: 3,
    title: 'コードレビュー',
    priority: 'high',
    dueDate: '2025-01-23',
    status: 'todo',
  },
  {
    id: 4,
    title: 'ミーティング準備',
    priority: 'medium',
    dueDate: '2025-01-24',
    status: 'in_progress',
  },
  {
    id: 5,
    title: 'バグ修正',
    priority: 'high',
    dueDate: '2025-01-22',
    status: 'todo',
  },
];

優先度とステータスに応じてタスクを並び替えます。

javascript// 優先度の定義
const priorityOrder = {
  high: 1,
  medium: 2,
  low: 3,
};

// ステータスの定義
const statusOrder = {
  in_progress: 1,
  todo: 2,
  done: 3,
};

// タスクをソート
const sortedTasks = orderBy(
  tasks,
  [
    // ステータス(進行中を優先)
    (task) => statusOrder[task.status],
    // 優先度(高を優先)
    (task) => priorityOrder[task.priority],
    // 期限(早い順)
    'dueDate',
  ],
  ['asc', 'asc', 'asc']
);

console.log(sortedTasks);

このソートにより、進行中の高優先度タスクで期限が迫っているものが最上位に表示されます。

シナリオ 4:ファイルエクスプローラー

ファイルエクスプローラーで、フォルダとファイルを適切な順序で表示する例です。

javascript// ファイル・フォルダデータ
const items = [
  {
    name: 'document10.txt',
    type: 'file',
    size: 2048,
    modified: '2025-01-15',
  },
  {
    name: 'images',
    type: 'folder',
    size: 0,
    modified: '2025-01-18',
  },
  {
    name: 'document2.txt',
    type: 'file',
    size: 1024,
    modified: '2025-01-20',
  },
  {
    name: 'downloads',
    type: 'folder',
    size: 0,
    modified: '2025-01-10',
  },
  {
    name: 'document1.txt',
    type: 'file',
    size: 512,
    modified: '2025-01-12',
  },
];

フォルダを先に表示し、その後ファイルを自然順でソートします。

javascript// 自然順ソート用関数(再掲)
function naturalSortKey(str) {
  return str.replace(/(\d+)/g, (match) => {
    return match.padStart(10, '0');
  });
}

// フォルダ優先、名前は自然順
const sortedItems = orderBy(
  items,
  [
    // type でソート(folder を先に)
    (item) => (item.type === 'folder' ? 0 : 1),
    // 名前を自然順でソート
    (item) => naturalSortKey(item.name),
  ],
  ['asc', 'asc']
);

console.log(sortedItems);
// フォルダが先に表示され、
// ファイルは document1.txt → document2.txt → document10.txt の順

パフォーマンスに関する注意点

大量のデータをソートする場合、パフォーマンスに注意が必要です。

javascript// パフォーマンステスト用の大量データ生成
function generateLargeDataset(count) {
  const dataset = [];
  for (let i = 0; i < count; i++) {
    dataset.push({
      id: i,
      name: `User${i}`,
      value: Math.floor(Math.random() * 1000),
    });
  }
  return dataset;
}

// 10万件のデータ
const largeData = generateLargeDataset(100000);

処理時間を計測してパフォーマンスを確認します。

javascript// パフォーマンス計測
console.time('orderBy');
const sorted = orderBy(largeData, ['value'], ['desc']);
console.timeEnd('orderBy');
// 通常は数十ms〜数百ms程度

パフォーマンスが問題になる場合の対策を以下の表にまとめました。

#対策説明効果
1ページネーションデータを分割して表示★★★
2メモ化ソート結果をキャッシュ★★☆
3Web Worker別スレッドで処理★★☆
4バックエンドソートサーバー側でソート★★★

エラーハンドリング

実務では、データが不完全な場合やエラーが発生する場合を想定する必要があります。

javascript// 安全なソート関数
function safeOrderBy(data, keys, orders) {
  try {
    // データの検証
    if (!Array.isArray(data)) {
      throw new TypeError(
        'Error: First argument must be an array'
      );
    }

    if (data.length === 0) {
      console.warn('Warning: Empty array provided');
      return [];
    }

    // ソート実行
    return orderBy(data, keys, orders);
  } catch (error) {
    console.error('Error in safeOrderBy:', error.message);
    return data; // エラー時は元のデータを返す
  }
}

この関数を使用することで、予期しないエラーを防ぐことができます。

javascript// 使用例
const result = safeOrderBy(users, ['age'], ['asc']);

// 空配列の場合
const emptyResult = safeOrderBy([], ['age'], ['asc']);
// Warning: Empty array provided

// 不正なデータ型の場合
const invalidResult = safeOrderBy(
  'not an array',
  ['age'],
  ['asc']
);
// Error in safeOrderBy: Error: First argument must be an array

TypeScript での型安全な実装

TypeScript を使用している場合、型定義を活用することでより安全なコードが書けます。

typescript// TypeScript での型定義
import { orderBy } from 'lodash';

// ユーザー型の定義
interface User {
  id: number;
  name: string;
  age: number;
  department: string;
}

// ソート順序の型
type SortOrder = 'asc' | 'desc';

// 型安全なソート関数
function sortUsers(
  users: User[],
  keys: Array<keyof User>,
  orders: SortOrder[]
): User[] {
  return orderBy(users, keys, orders);
}

使用時には型チェックが働き、誤ったキー名を指定するとコンパイルエラーになります。

typescript// 正しい使用例
const sorted = sortUsers(
  users,
  ['age', 'department'],
  ['asc', 'desc']
);

// エラーになる例(存在しないキー)
// const invalid = sortUsers(
//   users,
//   ['age', 'invalidKey'],  // TypeError: 'invalidKey' は存在しない
//   ['asc', 'desc']
// );

まとめ

この記事では、Lodash の orderBy 関数を使った、複雑なソート処理の実装方法を詳しく解説しました。

JavaScript 標準の sort メソッドでは複雑になりがちな比較ロジックも、orderBy を使えば宣言的かつ簡潔に記述できます。マルチキーソート、昇順・降順の混在、自然順ソートといった実務で頻繁に遭遇する要件に対して、シンプルで保守性の高いコードが書けるようになるでしょう。

特に、複数の条件を組み合わせたソートや、優先度の異なる条件を扱う場合、orderBy の真価が発揮されます。関数を使ったカスタムソートキーの指定により、柔軟な並び替えも実現できますね。

実務では、パフォーマンスやエラーハンドリングにも注意を払いながら、ユーザーにとって使いやすいソート機能を提供していきましょう。TypeScript を使用している場合は、型安全性を活かすことで、さらに堅牢なコードが書けます。

Lodash の orderBy をマスターすることで、データ操作の幅が大きく広がり、より洗練されたアプリケーション開発が可能になります。ぜひ、今回紹介したテクニックを実際のプロジェクトで活用してみてください。

関連リンク