[JavaScript] 自分が決めたルールで二次元配列を多重ソートしたい

[JavaScript] 自分が決めたルールで二次元配列を多重ソートしたい

実務案件などやってると、自分が決めたルールで多重ソートをしたくなる事が良くあるわけですが、それを JavaScript でやってみたいと思います。

やりたいこと

まずやりたい事は、データベースのテーブルのような二次元配列にソートをかけたいのだけど、自分の指定した値の順番でソートしたい。たとえば、ユーザー情報のリストがあったとしたときに、優先的に都道府県ごとのグループに整列させ、更に同じグループ内では男女別に並び替える。といった感じだ。

東京→神奈川→埼玉の順番に並んで、更に男性→女性の順番に整列されているコケシのイラスト

多重ソートする処理は、PHP とか MySQL とか ActionScript 3.0 とかではデフォルトで用意されていたと思う(忘れた)のだけど、ActionScriptJavaScript ない。ということで自作してます。

JavaScriptで配列内のエレメントをソート

今回やろうとしてることはちょっと複雑なので、まず ActionScript にある sortOn メソッドのような 配列内のエレメントをソート する処理を試してみます。

テストする配列はこれ

var datas=[
    {name:"河童",index:6},
    {name:"鎌鼬",index:5},
    {name:"烏天狗",index:3},
    {name:"牛鬼",index:4},
    {name:"酒呑童子",index:1},
    {name:"土蜘蛛",index:2},
    {name:"妖狐",index:0}
];

配列内のエレメントをソートするには、sort メソッドの引数に条件を比較するための関数を入れます。

//ソート
datas.sort(function(a,b){
    if(a["index"]<b["index"])return -1;
    if(a["index"]>b["index"])return 1;
    return 0;
});

//出力
for(var i=0,n=datas.length;i<n;i++){
    console.log(datas[i]["name"],datas[i]["index"]);
}

これを実行すると index 順に並びます。

妖狐 → 酒呑童子 → 土蜘蛛 → 烏天狗 → 牛鬼 → 鎌鼬 → 河童 の順に並んだ console.log

値の順番を指定して配列内のエレメントをソート

数字ではなく特定の値の順番で並び替えをする場合、たとえば以下のような配列があるとします。

var datas=[
    {name:"河童",type:"妖魔"},
    {name:"鎌鼬",type:"妖獣"},
    {name:"烏天狗",type:"妖魔"},
    {name:"牛鬼",type:"妖魔"},
    {name:"酒呑童子",type:"妖魔"},
    {name:"土蜘蛛",type:"妖魔"},
    {name:"妖狐",type:"妖獣"}
];

これを「妖魔 → 妖獣」という順番で並び替えたいので、順序用の配列 を用意して何番目の値かを元の配列に index として代入していきます。

//indexを代入
var rule=["妖魔","妖獣"];
for(var i=0,n=datas.length;i<n;i++){
    var index=rule.indexOf(datas[i]["type"]);
    datas[i]["index"]=index;
}

//ソート
datas.sort(function(a,b){
    if(a["index"]<b["index"])return -1;
    if(a["index"]>b["index"])return 1;
    return 0;
});

//出力
for(var i=0,n=datas.length;i<n;i++){
    console.log(datas[i]["name"],datas[i]["type"],datas[i]["index"]);
}

「妖魔 → 妖獣」という順番で並び替えられます。

河童 → 烏天狗 → 牛鬼 → 酒呑童子 → 土蜘蛛 → 鎌鼬 → 妖狐 の順に並んだ console.log

複数の順序ルールを指定して多重ソート

今度は、その値の順番を複数用意して、多重にソートをかけたいと思います。
配列内のオブジェクトの要素を増やしました。

var datas=[
    {name:"河童",type:"妖魔",attribution:"水"},
    {name:"鎌鼬",type:"妖獣",attribution:"風"},
    {name:"烏天狗",type:"妖魔",attribution:"風"},
    {name:"牛鬼",type:"妖魔",attribution:"水"},
    {name:"酒呑童子",type:"妖魔",attribution:"無"},
    {name:"土蜘蛛",type:"妖魔",attribution:"地"},
    {name:"妖狐",type:"妖獣",attribution:"火"}
];

並び替える順番は、まず「妖魔 → 妖獣」という順番で並び替えて、重複した値は「火 → 水 → 風 → 地 → 無」という順番で更に並び替えたいと思います。

で、ここからが本題で、最終的には index でソートするのですが、index の値を導くための公式を考えました。例えば、abcというカテゴリがあって、それぞれaの順序は配列Abの順序は配列Bcの順序は配列Cという配列のルールがあった場合、以下のような index の値を求める計算式が成り立ちます。

配列Aにおけるaの位置×配列Bの数×配列Cの数 + 配列Bにおけるbの位置×配列Cの数 + 配列Cにおけるcの位置

つまり、配列Aは typeの順が「妖魔 → 妖獣」、配列Bは attributionの順が「火 → 水 → 風 → 地 → 無」、配列Cは省略するので、以下のような計算をします。

「配列Aにおけるtypeの位置」×「配列Bの数」+「配列Bにおけるattributionの位置」

//indexを代入
var ruleA=["妖魔","妖獣"];
var ruleB=["火","水","風","地","無"];
for(var i=0,n=datas.length;i<n;i++){
    var indexA=ruleA.indexOf(datas[i]["type"]);
    var indexB=ruleB.indexOf(datas[i]["attribution"]);
    datas[i]["index"]=indexA*ruleB.length+indexB;
}

//ソート
datas.sort(function(a,b){
    if(a["index"]<b["index"])return -1;
    if(a["index"]>b["index"])return 1;
    return 0;
});

//出力
for(var i=0,n=datas.length;i<n;i++){
    console.log(datas[i]["name"],datas[i]["type"],datas[i]["attribution"],datas[i]["index"]);
}

最優先は「妖魔 → 妖獣」、続いて「火 → 水 → 風 → 地 → 無」という順番で並び替えられます。

河童 → 牛鬼 → 烏天狗 → 土蜘蛛 → 酒呑童子 → 妖狐 → 鎌鼬 の順に並んだ console.log

多重ソート関数

上記のことを踏まえまして、多重ソートができる関数を作りました。

msort(datas, rules)

第1引数(datas)
並び替えする対象の二次元配列
第2引数(rules)
並び替えルールを集めた配列
/**
 * 多重ソート関数
 * Aの位置×Bの数×Cの数+Bの位置×Cの数+Cの位置
 * @param datas {array} 並び替えする対象の二次元配列
 * @param rules {array} 並び替えルール
 */
function msort(datas,rules){
    if(!(datas instanceof Array))throw new Error('datas is not array.');
    if(!(rules instanceof Array))throw new Error('rules is not array.');

    //順序番号のパラメータを生成
    var p='_index_'+Math.floor(Math.random()*1000)+((new Date()).getTime()).toString();

    //並び替えする対象の二次元配列をループ
    for(var i1=0,n1=datas.length;i1<n1;i1++){
        var n2=rules.length;

        //並び替えの順番が指定されていないときに、datasから値を取り出して、通常のsortをした順序指定をrules変数内に上書きする
        for(var i2=0;i2<n2;i2++){
            var key=rules[i2][0];
            var rule=rules[i2][1];
            if(!(rule instanceof Array)){
                var tmp=new Array(n1);
                for(var i3=0;i3<n1;i3++){
                    tmp[i3]=datas[i3][key];
                }
                tmp.sort();
                if(rule=='desc')tmp.reverse();
                rules[i2][1]=new Array(n1);
                for(var i3=0;i3<n1;i3++){
                    rules[i2][1][i3]=tmp[i3];
                }
            }
        }
        
        //順序を計算する公式によって、順序番号の値を代入する
        var indexs=new Array(n2);
        for(var i2=0;i2<n2;i2++){
            var key=rules[i2][0];
            var rule=rules[i2][1];
            var index=rule.indexOf(datas[i1][key]);
            indexs[i2]=(index>-1)?index:rule.length;
            for(var i3=i2+1;i3<n2;i3++){
                var rule=rules[i3][1];
                if(rule instanceof Array){
                    indexs[i2]*=rule.length+1;
                }
            }
        }
        datas[i1][p]=0;
        for(var i2=0;i2<n2;i2++){
            datas[i1][p]+=indexs[i2];
        }
    }
    
    //順序番号の値でソートする
    datas.sort(function(a,b){
        if(a[p]<b[p])return -1;
        if(a[p]>b[p])return 1;
        return 0;
    });
}

並び替えルールの指定方法は以下より。

指定されたルールのソート

keyという項目から指定されたルールに並び替える

var rules=[
    ['key',["火","水","地","風","無"]]
];
msort(datas,rules);

文字コード順のソート

keyという項目から数字順/文字コード順に並び替える

var rules=[
    ['key']
];
msort(datas,rules);

降順のソート

keyという項目から数字順/文字コード順を降順に並び替える

var rules=[
    ['key','desc']
];
msort(datas,rules);

使用サンプル

多重ソート関数を確認できるサンプルページを用意しました。
もし、必要なときがあればぜひ参考にしてみて下さい。