JavaScript の sort() 関数をお勉強 : 複数のプロパティを見てソートする方法

JavaScript の Array.prototype.sort() を使って、連想配列の複数のプロパティを見てソートする方法を勉強した。

目次

対象データ

対象となるデータは、以下のようなモノ。company が会社名で、name がその会社の商品名だと思って欲しい。

const guitars = [
  { company: 'Gibson'  , name: 'Les Paul'            },
  { company: 'Gibson'  , name: 'Les Paul Custom'     },
  { company: 'Squire'  , name: 'Telecaster'          },
  { company: 'Squire'  , name: 'Stratocaster'        },
  { company: 'Gibson'  , name: 'SG Junior'           },
  { company: 'Gibson'  , name: 'SG Custom'           },
  { company: 'Gibson'  , name: 'SG'                  },
  { company: 'Fender'  , name: 'Mustang'             },
  { company: 'Fender'  , name: 'Telecaster Thinline' },
  { company: 'Fender'  , name: 'Jazz Master'         },
  { company: 'Epiphone', name: 'Les Paul'            },
  { company: 'Epiphone', name: 'Les Paul Custom'     },
  { company: 'Squire'  , name: 'Telecaster Thinline' },
  { company: 'Squire'  , name: 'Mustang'             },
  { company: 'Squire'  , name: 'Jazz Master'         },
  { company: 'Epiphone', name: 'SG'                  },
  { company: 'Fender'  , name: 'Stratocaster'        },
  { company: 'Fender'  , name: 'Telecaster'          },
  { company: 'Epiphone', name: 'SG Junior'           },
  { company: 'Epiphone', name: 'SG Custom'           },
];

エレキギターをかじった人なら分かるかもしれないが、「Gibson」と「Epiphone」、「Fender」と「Squire」は親・子会社 (ブランド) の関係で、例えば「Gibson」と「Epiphone」は同じ「Les Paul」という名前の製品を持っている (今回はあくまで一例なので、詳細なモデル名までは記していない)。

このようなオブジェクトの配列を並び替えて、会社名順 → 製品名順に並び替えたい、というのが今回の趣旨。

sort() 関数の比較ロジックは自作できる

まずは話をもっと単純にして、数値のみが入った配列に対して sort() を行うと、以下のようにソートしてくれる。

[5, 1, 3, 4, 2].sort();
// → [1, 2, 3, 4, 5]

ココで、sort() の第1引数に独自の比較ロジックを関数で与えると、並び替えのやり方を変えられる。

[5, 1, 3, 4, 2].sort((a, b) => {
  // 何が起こっているのか確認するためコンソール出力してみる
  console.log(`A : ${a} ・ B : ${b}`);
  
  // 通常の判定とは逆にしてみる
  if(a < b) return 1;
  if(a > b) return -1;
  return 0;
});

さて、コレを実行すると、以下のようになる。

A : 5 ・ B : 1
A : 1 ・ B : 3
A : 5 ・ B : 3
A : 1 ・ B : 4
A : 3 ・ B : 4
A : 5 ・ B : 4
A : 1 ・ B : 2
A : 3 ・ B : 2
[ 5, 4, 3, 2, 1 ]

sort() の第1引数に渡した関数の、仮引数 ab は、隣り合う要素を持ってきていることが分かる。コレに対し、比較の結果、-101 のいずれかを return してやることで、並び順が変えられる。上述の例では ab より小さい時に 1 を返し、その逆は -1 を返すようにしたので、結果が降順ソートになっている。

このように比較関数を渡さない場合は、内部的には要素が文字列やオブジェクトなどであっても <> で大小比較しているので、人間が思うような結果が得られない。そこで、各要素からプロパティを取り出し、その値を比較してソートしてやろう。

あ、ちなみに、Array.prototype.sort() は、対象の要素を直接並び替える。map() などのようにコピーした配列を返すワケではなく、元の配列に破壊的変更を加える関数なので留意。

まずは製品名だけでソートする

sort() 関数のカスタマイズということで、まずは製品名だけを見てソートするよう、比較関数を与えてやる。内部の動きが追いやすいよう、今回もコンソール出力させてみる。

const columnify = require('columnify');  // コンソール出力の整形用

// ソートしたいデータ
const guitars = [ /* 前述のとおり・省略 */ ];

// sort() 内の関数が呼ばれた回数をカウントする
let count = 0;
// あとで一括してコンソール出力するため、ソート中のデータを控えておく
let outputs = [];

guitars.sort((a, b) => {
  // カウンタのインクリメントとコンソール出力用の控え
  count++;
  outputs.push({
    'Count'    : count,
    'A:Company': a.company,
    'A:Name'   : a.name,
    'B:Company': b.company,
    'B:Name'   : b.name
  });
  
  // 製品名で比較する
  if(a.name < b.name) return -1;
  if(a.name > b.name) return  1;
  // 同一値なら 0 を返す
  return 0;
});

console.log('▼ ソート中のログ');
console.log(columnify(outputs, { columnSplitter: ' | ' }));
console.log('▼ ソート結果');
console.log(guitars);

結果は以下のとおり。

▼ ソート中のログ
COUNT | A:COMPANY | A:NAME              | B:COMPANY | B:NAME
1     | Gibson    | Les Paul            | Epiphone  | SG Custom
2     | Gibson    | Les Paul            | Epiphone  | Les Paul
3     | Squire    | Telecaster          | Gibson    | Les Paul
4     | Epiphone  | SG Junior           | Gibson    | Les Paul
5     | Fender    | Telecaster          | Gibson    | Les Paul
6     | Fender    | Stratocaster        | Gibson    | Les Paul
7     | Epiphone  | SG                  | Gibson    | Les Paul
8     | Squire    | Jazz Master         | Gibson    | Les Paul
9     | Squire    | Stratocaster        | Gibson    | Les Paul
10    | Squire    | Mustang             | Gibson    | Les Paul
11    | Squire    | Telecaster Thinline | Gibson    | Les Paul
12    | Epiphone  | Les Paul Custom     | Gibson    | Les Paul
13    | Gibson    | Les Paul Custom     | Gibson    | Les Paul
14    | Fender    | Jazz Master         | Gibson    | Les Paul
15    | Gibson    | SG Junior           | Gibson    | Les Paul
16    | Fender    | Telecaster Thinline | Gibson    | Les Paul
17    | Fender    | Mustang             | Gibson    | Les Paul
18    | Gibson    | SG                  | Gibson    | Les Paul
19    | Gibson    | SG Custom           | Gibson    | Les Paul
20    | Epiphone  | Les Paul            | Squire    | Jazz Master
21    | Epiphone  | Les Paul            | Fender    | Jazz Master
22    | Squire    | Jazz Master         | Fender    | Jazz Master
23    | Gibson    | SG Junior           | Epiphone  | SG Custom
24    | Epiphone  | SG Custom           | Squire    | Telecaster Thinline
25    | Gibson    | SG Junior           | Squire    | Telecaster Thinline
26    | Gibson    | SG                  | Gibson    | SG Junior
27    | Fender    | Mustang             | Gibson    | SG Junior
28    | Fender    | Telecaster Thinline | Gibson    | SG Junior
29    | Epiphone  | SG Junior           | Gibson    | SG Junior
30    | Squire    | Stratocaster        | Gibson    | SG Junior
31    | Fender    | Telecaster          | Gibson    | SG Junior
32    | Fender    | Stratocaster        | Gibson    | SG Junior
33    | Epiphone  | SG                  | Gibson    | SG Junior
34    | Gibson    | Les Paul Custom     | Gibson    | SG Junior
35    | Epiphone  | Les Paul Custom     | Gibson    | SG Junior
36    | Gibson    | SG Custom           | Gibson    | SG Junior
37    | Squire    | Mustang             | Gibson    | SG Junior
38    | Squire    | Telecaster          | Gibson    | SG Junior
39    | Squire    | Telecaster          | Squire    | Stratocaster
40    | Squire    | Telecaster          | Fender    | Stratocaster
41    | Squire    | Stratocaster        | Fender    | Stratocaster
42    | Squire    | Telecaster          | Fender    | Telecaster
43    | Fender    | Telecaster          | Fender    | Telecaster Thinline
44    | Fender    | Telecaster Thinline | Squire    | Telecaster Thinline
45    | Epiphone  | SG Custom           | Gibson    | SG
46    | Epiphone  | SG Custom           | Fender    | Mustang
47    | Gibson    | SG                  | Fender    | Mustang
48    | Epiphone  | SG Custom           | Epiphone  | SG
49    | Gibson    | SG                  | Epiphone  | SG
50    | Epiphone  | SG Custom           | Gibson    | Les Paul Custom
51    | Epiphone  | SG                  | Gibson    | Les Paul Custom
52    | Gibson    | SG                  | Gibson    | Les Paul Custom
53    | Fender    | Mustang             | Gibson    | Les Paul Custom
54    | Epiphone  | SG Custom           | Epiphone  | Les Paul Custom
55    | Epiphone  | SG                  | Epiphone  | Les Paul Custom
56    | Gibson    | SG                  | Epiphone  | Les Paul Custom
57    | Fender    | Mustang             | Epiphone  | Les Paul Custom
58    | Gibson    | Les Paul Custom     | Epiphone  | Les Paul Custom
59    | Epiphone  | SG Custom           | Gibson    | SG Custom
60    | Gibson    | SG Custom           | Squire    | Mustang
61    | Epiphone  | SG Custom           | Squire    | Mustang
62    | Epiphone  | SG                  | Squire    | Mustang
63    | Gibson    | SG                  | Squire    | Mustang
64    | Fender    | Mustang             | Squire    | Mustang

▼ ソート結果
[
  { company: 'Squire', name: 'Jazz Master' },
  { company: 'Fender', name: 'Jazz Master' },
  { company: 'Epiphone', name: 'Les Paul' },
  { company: 'Gibson', name: 'Les Paul' },
  { company: 'Gibson', name: 'Les Paul Custom' },
  { company: 'Epiphone', name: 'Les Paul Custom' },
  { company: 'Fender', name: 'Mustang' },
  { company: 'Squire', name: 'Mustang' },
  { company: 'Gibson', name: 'SG' },
  { company: 'Epiphone', name: 'SG' },
  { company: 'Epiphone', name: 'SG Custom' },
  { company: 'Gibson', name: 'SG Custom' },
  { company: 'Epiphone', name: 'SG Junior' },
  { company: 'Gibson', name: 'SG Junior' },
  { company: 'Squire', name: 'Stratocaster' },
  { company: 'Fender', name: 'Stratocaster' },
  { company: 'Squire', name: 'Telecaster' },
  { company: 'Fender', name: 'Telecaster' },
  { company: 'Fender', name: 'Telecaster Thinline' },
  { company: 'Squire', name: 'Telecaster Thinline' }
]

長ったらしいが、要素数20個の配列に対し、64回に渡って比較関数が実行され、製品名でソートできた。「ソート結果」を見ると、name プロパティの値が J・L・M・S・T… から始まる値で並んでいるのが分かるだろう。

複数の項目を見てソートするには?

さて、今回やりたいのは、まず会社名別にソートして、1つの会社内の製品もソートするという要件だ。コレを実現するには、以下のように実装する。

guitars.sort((a, b) => {
  // 先にグルーピングしたい「会社名」で比較する
  if(a.company < b.company) return -1;
  if(a.company > b.company) return  1;
  
  // 会社名が同一の場合、次に「製品名」で比較する
  if(a.name < b.name) return -1;
  if(a.name > b.name) return  1;
  
  // 同一値なら 0 を返す
  return 0;
});

コレを実行すると、以下のようになる。

▼ ソート中のログ
COUNT | A:COMPANY | A:NAME              | B:COMPANY | B:NAME
1     | Gibson    | Les Paul            | Epiphone  | SG Custom
2     | Epiphone  | SG Custom           | Epiphone  | Les Paul
3     | Squire    | Telecaster          | Epiphone  | SG Custom
4     | Epiphone  | SG Junior           | Epiphone  | SG Custom
5     | Fender    | Telecaster          | Epiphone  | SG Custom
6     | Fender    | Stratocaster        | Epiphone  | SG Custom
7     | Epiphone  | SG                  | Epiphone  | SG Custom
8     | Squire    | Stratocaster        | Epiphone  | SG Custom
9     | Squire    | Jazz Master         | Epiphone  | SG Custom
10    | Squire    | Mustang             | Epiphone  | SG Custom
11    | Squire    | Telecaster Thinline | Epiphone  | SG Custom
12    | Epiphone  | Les Paul Custom     | Epiphone  | SG Custom
13    | Gibson    | SG Junior           | Epiphone  | SG Custom
14    | Gibson    | Les Paul Custom     | Epiphone  | SG Custom
15    | Fender    | Jazz Master         | Epiphone  | SG Custom
16    | Fender    | Telecaster Thinline | Epiphone  | SG Custom
17    | Fender    | Mustang             | Epiphone  | SG Custom
18    | Gibson    | SG                  | Epiphone  | SG Custom
19    | Gibson    | SG Custom           | Epiphone  | SG Custom
20    | Epiphone  | Les Paul            | Epiphone  | SG
21    | Epiphone  | SG                  | Epiphone  | Les Paul Custom
22    | Epiphone  | Les Paul            | Epiphone  | Les Paul Custom
23    | Gibson    | SG Junior           | Gibson    | Les Paul
24    | Gibson    | Les Paul            | Squire    | Telecaster Thinline
25    | Gibson    | SG Junior           | Squire    | Telecaster Thinline
26    | Gibson    | SG                  | Gibson    | SG Junior
27    | Fender    | Mustang             | Gibson    | SG Junior
28    | Fender    | Telecaster Thinline | Gibson    | SG Junior
29    | Fender    | Jazz Master         | Gibson    | SG Junior
30    | Gibson    | Les Paul Custom     | Gibson    | SG Junior
31    | Squire    | Stratocaster        | Gibson    | SG Junior
32    | Epiphone  | SG Junior           | Gibson    | SG Junior
33    | Gibson    | SG Custom           | Gibson    | SG Junior
34    | Squire    | Mustang             | Gibson    | SG Junior
35    | Fender    | Telecaster          | Gibson    | SG Junior
36    | Squire    | Jazz Master         | Gibson    | SG Junior
37    | Fender    | Stratocaster        | Gibson    | SG Junior
38    | Squire    | Telecaster          | Gibson    | SG Junior
39    | Squire    | Telecaster          | Squire    | Jazz Master
40    | Squire    | Telecaster          | Squire    | Mustang
41    | Squire    | Jazz Master         | Squire    | Mustang
42    | Squire    | Telecaster          | Squire    | Stratocaster
43    | Squire    | Mustang             | Squire    | Stratocaster
44    | Squire    | Telecaster          | Squire    | Telecaster Thinline
45    | Gibson    | Les Paul            | Gibson    | SG
46    | Gibson    | SG                  | Fender    | Mustang
47    | Gibson    | Les Paul            | Fender    | Mustang
48    | Gibson    | SG                  | Fender    | Telecaster Thinline
49    | Gibson    | Les Paul            | Fender    | Telecaster Thinline
50    | Fender    | Mustang             | Fender    | Telecaster Thinline
51    | Gibson    | SG                  | Fender    | Jazz Master
52    | Gibson    | Les Paul            | Fender    | Jazz Master
53    | Fender    | Telecaster Thinline | Fender    | Jazz Master
54    | Fender    | Mustang             | Fender    | Jazz Master
55    | Gibson    | SG                  | Gibson    | Les Paul Custom
56    | Gibson    | Les Paul            | Gibson    | Les Paul Custom
57    | Gibson    | SG                  | Epiphone  | SG Junior
58    | Gibson    | Les Paul Custom     | Epiphone  | SG Junior
59    | Gibson    | Les Paul            | Epiphone  | SG Junior
60    | Fender    | Telecaster Thinline | Epiphone  | SG Junior
61    | Fender    | Mustang             | Epiphone  | SG Junior
62    | Fender    | Jazz Master         | Epiphone  | SG Junior
63    | Gibson    | SG                  | Gibson    | SG Custom
64    | Gibson    | SG Custom           | Fender    | Telecaster
65    | Gibson    | SG                  | Fender    | Telecaster
66    | Gibson    | Les Paul Custom     | Fender    | Telecaster
67    | Gibson    | Les Paul            | Fender    | Telecaster
68    | Fender    | Telecaster Thinline | Fender    | Telecaster
69    | Fender    | Mustang             | Fender    | Telecaster
70    | Gibson    | SG Custom           | Fender    | Stratocaster
71    | Gibson    | SG                  | Fender    | Stratocaster
72    | Gibson    | Les Paul Custom     | Fender    | Stratocaster
73    | Gibson    | Les Paul            | Fender    | Stratocaster
74    | Fender    | Telecaster Thinline | Fender    | Stratocaster
75    | Fender    | Telecaster          | Fender    | Stratocaster
76    | Fender    | Mustang             | Fender    | Stratocaster

▼ ソート結果
[
  { company: 'Epiphone', name: 'Les Paul' },
  { company: 'Epiphone', name: 'Les Paul Custom' },
  { company: 'Epiphone', name: 'SG' },
  { company: 'Epiphone', name: 'SG Custom' },
  { company: 'Epiphone', name: 'SG Junior' },
  { company: 'Fender', name: 'Jazz Master' },
  { company: 'Fender', name: 'Mustang' },
  { company: 'Fender', name: 'Stratocaster' },
  { company: 'Fender', name: 'Telecaster' },
  { company: 'Fender', name: 'Telecaster Thinline' },
  { company: 'Gibson', name: 'Les Paul' },
  { company: 'Gibson', name: 'Les Paul Custom' },
  { company: 'Gibson', name: 'SG' },
  { company: 'Gibson', name: 'SG Custom' },
  { company: 'Gibson', name: 'SG Junior' },
  { company: 'Squire', name: 'Jazz Master' },
  { company: 'Squire', name: 'Mustang' },
  { company: 'Squire', name: 'Stratocaster' },
  { company: 'Squire', name: 'Telecaster' },
  { company: 'Squire', name: 'Telecaster Thinline' }
]

今度は比較関数が76回呼ばれている (先程は64回)。会社名での比較も入れたので、ソート中の動きが変わっている。

そして結果は、狙ったとおり、会社名別に「E・F・G・S…」始まりで並び、会社内の製品も「L・S…」「J・M・S・T…」始まりで上手くソートされているのが分かる。

以上

このように、複数のキー (条件) を使ってのソートも、比較的簡単に実装できることが分かった。