【jsの落とし穴】sort()によるソート実装を行うとChromeで正しくソートされない件について
Web2017年8月18日
以前の記事でsort()メソッドを使ってコンテンツを並べ替えるjsをご紹介させていただきましたが、Chromeのみ他のブラウザと比べて若干ソート結果が異なるケースがあります。本記事ではsort()によるソートにおいて、Chromeのみソート結果が異なってしまった場合の対処方法についてご紹介させていただきたいと思います。
サンプルコード
- ※Chromeのみ、ソート結果が異なるjs
js
$(function(){
'use scrict';
$('ul.test_1 li').each(function(){
var $date = $(this).attr('data-date');
dateFunc = function(){
$date = $date.replace(/-| /g, ':').split(':');
$date = new Date($date[0],$date[1]-1,$date[2],$date[3],$date[4],$date[5]);
return $date;
};
dateFunc($date);
date = $date.getTime();
$(this).attr('data-date',date);
});
$('ul.test_1').html(
$('ul.test_1').find('> li').sort(function(a,b){
return $(b).attr("data-date") - $(a).attr('data-date');
})
);
});
- ※jQueryを読み込んでいる場合のスクリプトとなります
html
<ul class="list-bullet-01 test_1">
<li data-date="2017-08-18 00:00:00">2017年8月18日</li>
<li data-date="2017-08-15 00:00:00">2017年8月15日</li>
<li data-date="2017-08-23 00:00:00">2017年8月23日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(1)</li>
<li data-date="2017-08-21 00:00:00">2017年8月21日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(2)</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(3)</li>
<li data-date="2017-08-19 00:00:00">2017年8月19日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(4)</li>
<li data-date="2017-08-26 00:00:00">2017年8月26日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(5)</li>
</ul>
サンプルコード
- ※Chrome含め、全ブラウザ同じソート結果になるjs
js
$(function(){
'use scrict';
$('ul.test_2 li').each(function(){
var $date = $(this).attr('data-date');
var $liIndex = $(this).index();//追加記述
dateFunc = function(){
$date = $date.replace(/-| /g, ':').split(':');
$date = new Date($date[0],$date[1]-1,$date[2],$date[3],$date[4],$date[5]);
return $date;
};
dateFunc($date);
date = $date.getTime();
date = parseInt(date) - $liIndex;/追加記述
$(this).attr('data-date',date);
});
$('ul.test_2').html(
$('ul.test_2').find('> li').sort(function(a,b){
return $(b).attr("data-date") - $(a).attr('data-date');
})
);
});
- ※jQueryを読み込んでいる場合のスクリプトとなります
html
<ul class="list-bullet-01 test_2">
<li data-date="2017-08-18 00:00:00">2017年8月18日</li>
<li data-date="2017-08-15 00:00:00">2017年8月15日</li>
<li data-date="2017-08-23 00:00:00">2017年8月23日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(1)</li>
<li data-date="2017-08-21 00:00:00">2017年8月21日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(2)</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(3)</li>
<li data-date="2017-08-19 00:00:00">2017年8月19日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(4)</li>
<li data-date="2017-08-26 00:00:00">2017年8月26日</li>
<li data-date="2017-08-20 00:00:00">2017年8月20日(5)</li>
</ul>
解説
前者のスクリプトを実行すると、日時単位でのソートは各ブラウザで問題なく行われます。しかし、Chromeのみ同じアイテム(サンプルの(1)~(5))については元の序列が維持されず、入れ替わりが起こってしまいます。
- ※Firefox等、他のブラウザでは同じ値の序列は維持される
これはChromeが積んでいるV8エンジンが、クイックソートなる仕様を備えていることによるものだそうで、sort()による置換を行う場合は「return a - b」による評価関数の結果をもって並べ替えが行われますが、「return 0」値として返ってきた場合の挙動がChromeのみ順番が保証されなくなるようです。
実案件でこのケースに遭遇した際は頭を抱えましたが、幸い今回日付(ミリ秒)による比較を行ったうえでの並べ替えでしたので、以下の方法で解決することができました。
ミリ秒をparseInt()で数値化、さらにliのインデックス番号を減算して全てのミリ秒を完全ユニーク化
面白いロジックだなあと自分でも思いますが、思いがけない方法で解決することができました。後者のスクリプトでは、以下の記述を加筆しています。
var $liIndex = $(this).index();
date = parseInt(date) - $liIndex;
liのインデックス番号を変数$liIndexに格納しておきます。さらに、ミリ秒の値が既に格納されている変数dateをparseInt()で数値化し、$liIndexを減算します。これにより、同じミリ秒の値であってもインデックス番号は絶対に異なるため、結果として全ての値をユニーク化することができ、return 0は返ってこなくなります。完全に数値による並べ替えが行われるためブラウザ間での差異はなくなり、ソート前の序列も維持されることになります。
デモ
実際に動作を確認できるデモページも公開しておりますのであわせてご確認ください。
数値が小さい場合はこの方法で対処するのは難しい
ご紹介したケースのように、桁数の多いミリ秒(例:1502982000000)の場合は問題ありませんが、1~100など桁数の少ない数字では使えないでしょう。減算されるインデックス番号の数によっては数の大小が入れ替わってしまい、結果として正しいソート順にならない可能性が考えられるためです。
同じ値をさらに配列化して対応する方法もあるみたいです
実案件では採用を断念しましたが、同じ値を発見したらそれらをさらに配列化することで順番を定義し並べ替える、という方法でも対処できるようです。ググッたところ以下のページが参考になりそうでしたので、リンク先をご紹介させていただきます。
- javascript - Chromeでの文字列配列ソートがおかしい - スタック・オーバーフロー
- Unstable vs. Stable Sorting - JSFiddle
- JavaScript の Array#sort で return 0 すると要素の順番は不定になります
- JSのオブジェクトをソートする(chrome対応) - jsdo.it
まとめ
Chromeだけsort()の結果が異なる、というのは何年も前から起きている問題のようです。元々同じ値が存在しない前提のソートであれば特に気にする必要はないのですが、ご紹介したような、全く同じ日時や値の場合を考慮して各ブラウザのソート結果を同じにするには、代替手段を検討する必要があります。もしくは実装前にChromeだけ同じ値の序列は不定になってもかまわないですか、ということを関係者と握っておくのも一つので手ですかね。それで問題ないのであれば、対処する必要もなくなります。
もしsort()によるソート実装でChromeだけソート結果が異なる事象に遭遇した際は、本記事がご参考になれば幸いです。