JS:配列の正しいソート方法

JS:配列の正しいソート方法

author : koki

publish date :

配列のソートで困ったことはありませんか?実際たまにしか使わないので、自分の備忘録を見返しています。
ということで、少し説明を加えて備忘録をシェアします。

説明はいらないから、必要なことだけ知りたい方は目次からご希望の箇所に飛んでください。

.sort()の基本

まず、.sort()の構文は以下になります。

arr.sort([compareFunction])
引数compareFunctionには、ソート順を定義する関数を指定します。省略された場合、配列は各要素の文字列比較に基づき辞書順にソートされます。

Array.prototype.sort() - JavaScript | MDN

ここで言う「各要素の文字列比較に基づき辞書順にソート」ですが、文字列をUnicodeへ変換した時の数値です。
引数を持たせない場合の.sort()では単純に文字をUnicodeに変換した値でソートしてしまうので、数値のソートや大文字、小文字を含むアルファベットのソート、ひらがな、カタカナを含むソートをする際に問題が出てきます。

単純のソート

引数を持たせないソートの場合は、「アルファベット小文字のみ」、「アルファベット大文字のみ」、「ひらがなのみ」、「カタカナのみ」のような単純な比較しか出来ません。エラーが起きるわけではないのですが、引数を持たせずにソートしてしまうと思い通りの結果を得れないのです。
数字、アルファベット、日本語を例に上げてみます。

10以上の数値を含む配列をソートした場合、

var arr = [8,10,3,6,1,4,5,7,9,11,2];
arr.sort();

結果は[1,10,11,2,3,4,5,6,7,8,9]となり、数学的な並び順ではありません。

大文字、小文字を含む配列をソートした場合

var arr = ['pineapple','Orange','kiwi','Banana','apple'];
arr.sort();

結果は['Banana','Orange','apple','kiwi','pineapple']となり、アルファベット順ではありません。

ひらがな、カタカナを含む配列をソートした場合

var arr = ['バナナ','パイナップル','みかん','キウイ','アップル'];
arr.sort();

結果は['みかん','アップル','キウイ','バナナ','パイナップル']となり、五十音順の並び順ではありません。

漢字を含む配列をソートした場合

var arr = ['九','六','四','二','八','十','一','七','五','三'];
arr.sort();

結果は['一','七','三','九','二','五','八','六','十','四']となり、期待する並び順ではありません。

このようなソートをする場合には、.sort()の引数にソートするルールとなる比較関数を定義し、ソートさせることが出来ます。

比較関数の作り方

比較関数の作り方ですが、

aとbを比較し、
b→aとしたい(bの順序を前の方に上げる)場合はreturnで正の数を返し、
a→bとしたい(bの順序を後の方に下げる)場合はreturnで負の数を返し、
順序を変更しない場合はreturnでゼロを返します。
この説明では、理解が難しいと思うので具体的な説明は実際のサンプルとともに説明をします。

また、比較関数を書く場所ですが、無名関数を.sort()内に直接書かれているスクリプトをよく目にしますが、関数を外部化し、関数を指定しても問題ありません。

無名関数を直接記述した.sort()

arr.sort(function(a, b){
	// 処理をここに書く
});

外部化された関数を指定した.sort()

arr.sort(sortLogic);
function sortLogic(a, b){
	// 処理をここに書く
}
  • 同じ比較関数を複数箇所で使う場合は関数を外部化したほうがソースコードを減らせます。

【完成版】比較関数

先に例に上げたように、ソートする内容によっては.sort()の引数に比較関数を指定しないと思い通りのソートが出来ません。ここで私がよく使っている比較関数をご紹介します。

数字のソート

数字のソートをに使う比較関数はこれだ!

arr.sort(function(a, b){
	return a - b;
});

これで、数字順のソートの[1,2,3,4,5,6,7,8,9,10,11]と数字順にならびます。

もう少し詳しく説明

コードが長ったらしくなってしまいますが、理解しやすく書くとこのようになります。

function compare(a, b){
	if(a < b){ // aがbより小さい場合
		return -1;
	}
	if(a > b){ // aがbより大きい場合
		return 1;
	}
	// aとbが同じだった場合
	return 0;
}

この長いコードと前記の短いコードは同じ動きとなります。
短いコードの方ではa-bと計算して、その答えをそのままreturnしているのですが、計算結果そのものが正の数、負の数、ゼロとなっているのでたった1行で比較関数のロジックとして成り立っています。

アルファベット(大文字、小文字)のソート

アルファベットのソートをに使う比較関数はこれだ!

arr.sort(function(a, b){
	a = a.toString().toLowerCase();
	b = b.toString().toLowerCase();
	if(a < b){
		return -1;
	}else if(a > b){
		return 1;
	}
	return 0;
});

これで、['apple','Banana','kiwi','Orange','pineapple']とアルファベット順にならびます。

もう少し詳しく説明

くどいようですが、.sort()は要素の文字をUnicodeに変換し、その数値でソートします。
そこで、AからZ、そしてaからzのUnicodeを.charCodeAt()調べると、
「A」が65、「B」が66、「C」が67・・・「Z」は90と番号が振られていて、
「a」が97、「b」は98、「c」は99・・・「z」は122と番号が振られています。
文字が登録されている順番に差があるので、文字列を.toLowerCase()で小文字に変換し、ソートをするようにします。

ひらがな、カタカナの五十音順でのソート

ひらがな、カタカナのソートをに使う比較関数はこれだ!

arr.sort(function(a, b){
	a = katakanaToHiragana(a.toString());
	b = katakanaToHiragana(b.toString());
	if(a < b){
		return -1;
	}else if(a > b){
		return 1;
	}
	return 0;
});
// https://gist.github.com/kawanet/5553478
/** カタカナをひらがなに変換する関数
 * @param {String} src - カタカナ
 * @returns {String} - ひらがな
 */
function katakanaToHiragana(src) {
	return src.replace(/[\u30a1-\u30f6]/g, function(match) {
		var chr = match.charCodeAt(0) - 0x60;
		return String.fromCharCode(chr);
	});
}

これで、['アップル','キウイ','バナナ','パイナップル','みかん']と五十音順にならびます。

もう少し詳しく説明

アルファベットと同様で、ひらがなとカタカナも登録されているUnicodeが違うので、文字列をひらがなに変換をしてからソートをするようにします。
カタカナをひらがなに変換する関数はYusuke KawasakiさんがGitHub上で公開しているhiragana-katakana.jsより使わせて頂いてます。
非常に助かりました、ありがとうございます。

漢字のソート

漢字のソートをしたい場合は一筋縄ではいきません。
なぜなら、漢字はUnicodeの順番が部首の画数順で登録されている[1]ようなので、プログラムにソートをさせることは出来ません。
「一」~「十」のUnicodeを調べてみても下記表のようにバラバラと登録されていました。

漢字Unicode
19968
20108
19977
22235
20116
20845
19971
20843
20061
21313

なので、とてつもなく面倒ですがソートしたい漢字をリストアップし、希望の並び順を定義する必要があります。
ちょっと実用性が低いのですが、ここでは「一」~「十」までをソートできる比較関数をご紹介します。

arr.sort(function(a, b){
	var kanji = ['一','二','三','四','五','六','七','八','九','十'];
	var c = kanji.indexOf(a);
	var d = kanji.indexOf(b);
	return c - d;
});

これで、['一','二','三','四','五','六','七','八','九','十']となります。

もう少し詳しく説明

今回は、配列kanjiで漢字をソートしたい順番に定義し、.indexOf()で該当す漢字の位置を確かめ、その順番に基づきソートさせる方法です。

最後に・・・

配列のソートに限った話では無いのですが、他にも使いまわせそうな機能をもった関数を作った時はどこかにまとめておいて、毎回そこから使うようにしたほうがいいと思います。
ただまとめるだけではなく、バグを発見したらアップデートしていってくださいね。

ではでは、よき開発ライフを~。