配列・連想配列関数・メソッド

【JavaScript】 sort()メソッドを使用した配列並び替えを極める

更新日:2021/01/18

配列の要素を並び変えたいときsort()メソッドを使用する。
しかし僕の場合sort()の使い方を毎回ネットで調べている。

そろそろ調べなくても使えるようになりたいので、記事にしてみた。

 

Array.prototype.sort()メソッド

Array.prototype.sort()は、配列の要素を順番に並び変えるメソッドです。

次のように、文字列配列に対して実行することで、簡単に並び替えをおこなうことができます。


const array=["けーちゃん","あーちゃん","とらちゃん"];
array.sort( );
console.log(array); // 結果:[ "あーちゃん", "けーちゃん", "とらちゃん" ]

しかし、sort()メソッドの真価は、コールバック関数を引数として渡すことで、もっと高度な並び替えができる点です。

Array.prototype.sort()の構文

Array.prototype..sort( [callBack] )

callBackは省略可能です。
次の形式の関数を指定します。

function(a,b){} または (a,b)=>{}

abは配列の要素が渡され、関数の返り値により入れ替えがおこなわれます。
入れ替えの条件は次のようになっています。

  • 返り値が0のとき、そのまま
  • 返り値が0未満(マイナス)のとき、a→bの順番に入れ替える
  • 返り値が0より大きい(プラス)のとき、b→aの順番に入れ替える

これは結局のところ、引数の順番を基準としたとき、「プラス値を返すと順番が入れ替わる」と覚えておくといいでしょう。

 

Array.prototype.sort()メソッドの注意点

.sort()メソッドは、配列の中身が直接並び変えられます。
次のように、並び替えの前のデータを保存しようとしても無効です。


const array=["けーちゃん","あーちゃん","とらちゃん"];
const arrayOld = array;
array.sort( );
console.log(arrayOld); // 結果:[ "あーちゃん", "けーちゃん", "とらちゃん" ]

保存したつもりのarrayOldも、並び変えられています。
JavaScriptの変数は参照値が格納されているので、arrayとarrayOldは同じものを参照しています。
arrayの参照先が並び変えられたら、同じものを参照しているarrayOldも並び変えられていることになります。

今回のような目的には、Array.from()を使用してarrayを元にした新規配列を作成します。


const array=["けーちゃん","あーちゃん","とらちゃん"];
const arrayOld = Array.from(array); // arrayを元にした新規配列を作成
array.sort( );
console.log(arrayOld); // 結果:["けーちゃん","あーちゃん","とらちゃん"]

 

sort()の基本形

sort()メソッドを使用するとき、次の基本形を覚えておけば全てに応用できます。

次の形式が昇順(小さい値から大きい値の順)並び替えの基本形となります。

昇順並び替えの基本形

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

// アロー関数
array.sort( ( a , b ) => (a) - (b) );

例:


[5,3,10].sort( (a , b ) => ( a - b ) ); // [3,5,10]

[ {data:5},
  {data:3},
  {data:10} ].sort( (a , b ) => ( a.data - b.data ) ); // [{data:3},{data:5},{data:10}]

(a)(b) は、引数として受け取ったaとbを、数値に変換することをあらわしています。
関数であるかのようにかいてありますが、数値として評価できるなら関数である必要はありません。

a.プロパティ - b.プロパティメソッド(a) - メソッド(b)など、結果が数値になるように修正してください。

降順(大きい値から小さい値の順)に並び変えるときは、昇順の計算結果に-1を掛け算します。
比較する順番を逆にするとか考える必要はありません。
-1をかけるだけです。

降順並び替えの基本形

array.sort( function( a , b ){
                        return ((a) - (b)) * -1;
                 });

// アロー関数
array.sort( ( a , b ) => ((a) - (b)) * -1 );

例:


[5,3,10].sort( (a , b ) => (a - b) * -1 ); // [10,5,3]

 

sort()の例

上の基本形を利用して、並び替えの例をいくつか挙げてみます。

なお、ここでは昇順での並び替えについて例示してあります。
降順で並べる場合は、結果に-1を掛けてください。

文字列の長さで並び替え

文字列の長さで並び替えをおこなうには、次のようにlengthプロパティで計算します。


const array=["わんこ","ねこ","皇帝ペンギン"];
array.sort( (a,b)=>a.length - b.length);
console.log( array ); // [ "ねこ", "わんこ", "皇帝ペンギン" ]

ただし、絵文字などを使用している場合上手くソートできません。


const array=["わんこ","ねこ","皇帝ペンギン","🐶🐶"];
array.sort( ( a , b ) => a.length - b.length);
console.log( array ); // [ "ねこ", "わんこ", "🐶🐶", "皇帝ペンギン" ]

2文字の"🐶🐶"が、3文字の"わんこ"の後にソートされてしまいました。
これは1つの"🐶"が2文字としてカウントされているからです。

どうしてそうなるかは、次のページをみてください。
【JavaScript】 文字列データの内部形式と関連メソッドについてまとめてみた

上の記事でも解説していますが、絵文字や古い漢字など一部の特殊な文字を含む可能性がある場合は次のようスプレッド構文を使用して計算する必要があります。


const array=["わんこ","ねこ","皇帝ペンギン","🐶🐶"];
array.sort( ( a , b ) => [...a].length - [...b].length );
console.log( array ); //  [ "ねこ", "🐶🐶", "わんこ", "皇帝ペンギン" ]

オブジェクトのプロパティで並び変える

オブジェクトのプロパティを使用して、並び変えることができます。


const obj = [
    { data:10 },
    { data:20 },
    { data:3 },
};

obj.sort( ( a , b ) => a.data - b.data );
console.log( obj ); // [ { data:3 } , { data:10 } , { data:20 }

プロパティは計算可能である必要があります。

オブジェクトを複数条件で並び変える

学年、クラス、出席番号を持っているオブジェクトを並び変えてみます。


const student = function (schoolYear,schoolClass,classNumber){
        this.schoolYear=schoolYear;       // 学年
        this.schoolClass=schoolClass;    // クラス
        this.classNumber=classNumber;  // 出席番号
};

const students = [
        new student(3,2,5),new student(2,2,3),new student(3,1,5),
        new student(1,3,3),new student(2,1,1),new student(3,2,4),
        new student(1,2,1),new student(1,3,1),new student(3,2,1),
];

students.sort( ( a , b ) => {
        if( a.schoolYear !== b.schoolYear ) return a.schoolYear - b.schoolYear;
        if( a.schoolClass !== b.schoolClass ) return a.schoolClass - b.schoolClass;
        return a.classNumber - b.classNumber;
});

console.log( students );
// 結果:
// [
//    { schoolYear: 1, schoolClass: 2, classNumber: 1 }
//    { schoolYear: 1, schoolClass: 3, classNumber: 1 }
//    { schoolYear: 1, schoolClass: 3, classNumber: 3 }
//    { schoolYear: 2, schoolClass: 1, classNumber: 1 }
//    { schoolYear: 2, schoolClass: 2, classNumber: 3 }
//    { schoolYear: 3, schoolClass: 1, classNumber: 5 }
//    { schoolYear: 3, schoolClass: 2, classNumber: 1 }
//    { schoolYear: 3, schoolClass: 2, classNumber: 4 }
//    { schoolYear: 3, schoolClass: 2, classNumber: 5 }
// ]

学年が同じではない、つまり差が0ではないなら差をリターン。
同じなら、次はクラスが同じではないなら、差をリターン。
同じなら、次は出席番号の差をリターンしています。

これで、学年、クラス、出席番号の順番に並び変えることができます。

日付の並び替え

全く形式が同じ日付文字列は、次のように並び替えができます。


const array=["2020/10/10 PM10:10:10","2010/01/01","2039/05/06","2018/06/08","2020/10/10 AM10:10:10"];
array.sort( ( a , b ) => a > b ? 1 : -1);
console.log( array ); 
// [ "2010/01/01", "2018/06/08", "2020/10/10 AM10:10:10", "2020/10/10 PM10:10:10", "2039/05/06" ]

全く同じと言いつつ一部時刻が入っていますが、ようするに文字として比較しているだけです。
特に"AM"と"PM"は文字の並び順で"AM"→"PM"となるので、運よく順番に並んでくれています。
他の文字だったら逆になってしまう可能性があるので、確認しておく必要がありますね。

日付の形式が要素によって異なったり、文字列では上手く並べないときは、Dateオブジェクトに変換して計算する方法もあります。


const convertDateString = dt => dt.replace( /(AM|PM)(\d\d)/,( e , p1 , p2 )=>
        (parseInt(p2) + ( p1 === "PM"  ? 12 : 0) ).toString()
);

const array=["2020/10/10 PM10:10:10","2010/01/01","2039/05/06","2018/06/08","2020/10/10 AM10:10:10"];
array.sort( (a,b)=>new Date(convertDateString(a)) - new Date(convertDateString(b)));
console.log( array );

DateコンストラクターがAM/PMを受け入れてくれないので、24時間に変換してからDateインスタンスを作成しています。

また、Dateインスタンスは計算式と関連するとき、1970年1月1日 00:00:00からのミリ秒を返します。


const dt = new Date("2020/10/10 10:10:10");
console.log( dt ); //  Sat Oct 10 2020 10:10:10 GMT+0900 (日本標準時)
console.log( dt * 1); // 1602292210000

そのため、特にプロパティを呼び出さなくても直接計算することができます。

2次元配列の並び替え

2次元配列に関連したソートをいくつか挙げてみます。

各2次元配列要素をソートする

const array=[
        [5,2,4,3],
        [2,3,1,7],
        [6,3,7,0],
];
array.forEach( e=>e.sort( ( a , b ) => a-b) );

console.log( array );
// [
//     [2, 3, 4, 5],
//     [1, 2, 3, 7],
//     [0, 3, 6, 7],
// ]

forEachで各2次元配列要素を順番に呼び出し、並び替えをおこなっています。

元配列を2次元配列の最終要素でソート

const array=[
        [5,2,4,3],
        [2,3,1,7],
        [6,3,7,0],
];
array.sort( ( a , b ) => a[a.length-1] - b[b.length-1]) ;

console.log( array );
// [
//     [6,3,7,0],
//     [5,2,4,3],
//     [2,3,1,7],
// ]

考え方は、オブジェクトプロパティでの並び変えと同じです。

元配列を2次元配列の最終要素でソートし2次元配列もソート

2次元配列内をソートしつつ、2次元配列の総和で元配列をソートしてみる。

まずは、効率が悪い例。


const sum = (data) => data.reduce( ( a ,b ) => a + b );

const array=[
        [5, 2, 4, 3],
        [2, 3, 1, 7],
        [6, 3, 7, 0],
    ];
array.sort( ( a , b ) => {
        a.sort( (a1 , a2 ) => a1 - a2 );
        b.sort( (b1 , b2 ) => b1 - b2 );
        return sum(a) - sum(b);
    });

console.log( array );
// [
//     [1, 2, 3, 7],
//     [2, 3, 4, 5],
//     [0, 3, 6, 7],
// ]

sumは、配列要素の総和を求める関数です。

sort()のコールバック関数内では、引数として渡された配列内をソートしたあと、各配列の総和の差をリターンしています。

このコードは正しい結果が得られるので特に問題がないと感じますが、非常に効率が悪いです。
なぜなら並び替えをおこなうために、同じ要素が何度もコールバック関数に渡されるからです。

上の例では、次のように要素が渡されています。

1回目: 0番目 と 1番目
2回目: 0番目 と 2番目

1回目と2回目で、同じ0番目の要素が渡されています。
つまり0番目に対して内部ソートと総和の計算を2回おこなっています。

今回のデータは要素数が少ないので2回のみですが、要素数が多くなるとさらに多くの同じ計算をおこなうことになります。
無駄なので、1回で済むようにコードを構築してみます。


const arraySort = arry => {
        const wSet = new WeakMap();
        arry.forEach( e=> {
            e.sort( ( e1 , e2 ) => e1 - e2 );
            wSet.set( e , sum(e) );
        });
        arry.sort( ( a , b ) => wSet.get(a) - wSet.get(b));
        return arry;
    };

console.log( arraySort(array) );
// [
//     [1, 2, 3, 7],
//     [2, 3, 4, 5],
//     [0, 3, 6, 7],
// ]

まずは2次元配列内の要素を並び変えてしまいます。
その際、総和も計算しておきます。
その計算結果の保存先に便利なのが、WeakMapオブジェクトです。

WeakMapはオブジェクトをキーとして任意の値を保存できます。
配列もオブジェクトなので問題なく使用できます。

最後に計算した総和をWeakMapから取り出しながら、ソートします。

これでムダな処理なく並び替えをおこなうことができます。

更新日:2021/01/18

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。