javascriptで配列を指定したキーワード順にソートする

初めて書いたqiitaの記事、数ストックでも嬉しくて、数時間おきに覗きに行ってたのは秘密の話です。
今回もmacで頑張って入力します。commandの位置が微妙でショートカットが入力しづらいと思ってるのは僕だけですかねすみません。。

# 本題
前回に引き続き、javascriptでデータを処理するお話。
指定したキーのデータを、自然ソートではなくて、指定したキーワード順にソートしたい！って要件がありました。
A,B,C..をA,B,C..と並べるのではなく、C,A,B..と並べるようなのです。

調べましたがそれらしい関数が見当たらなかったので、作ってみました。

# ソースコードはよ

***2016/06/19追記： 最下部に頂戴したコメントの内容を反映した版を記載しましたので参考にしてください***

```keywordsorter.js
// 指定したキーワード順に配列をソートする
keywordsorter = function(key,keywords,array) {
    var self = this;

    // ソート順を取得する
    self.getSequenceNumber = function(a) {
        var seq = keywords.indexOf(a[key]);
        return seq >= 0 ? seq : array.length;
    };

    // ソート結果を返す
    return array.sort(function(a,b) {
        var seqA = self.getSequenceNumber(a);
        var seqB = self.getSequenceNumber(b);
        if( seqA === seqB ) {
            return 0;
        }
        return seqA > seqB ? 1 : -1;
    });
};
```


大した工夫もありませんが、ソートしたいキーワード順を示す配列に対してArray.indexOf()でソート順を取得するようにしました。また、Array.indexOf()では、キーワードが存在しない場合に -1 が返ってきてしまい、そのままソート順に使うと先頭にデータを持っていく可能性がありましたので、キーワードがない場合は、ソート対象の配列の length を返すことで、ソート後にデータが後方に並ぶようにしました。

# テスト

テストデータは、前回に引き続き、日本プロ野球2015年シーズンの規定打席到達打者一覧です。今回使わないような項目もありますが、単にテストデータの作成をサボりました。

```js
// テストデータ
var npbScores = [
        {"year":2015,"name":"川端　慎吾","team":"(ヤ)","dasuu":581,"hit":195,"homerun":8,"daten":57,"steal":4},
        {"year":2015,"name":"山田　哲人","team":"(ヤ)","dasuu":557,"hit":183,"homerun":38,"daten":100,"steal":34},
        {"year":2015,"name":"筒香　嘉智","team":"(デ)","dasuu":496,"hit":157,"homerun":24,"daten":93,"steal":0},
        {"year":2015,"name":"ルナ","team":"(中)","dasuu":496,"hit":145,"homerun":8,"daten":60,"steal":11},
        {"year":2015,"name":"ロペス","team":"(デ)","dasuu":516,"hit":150,"homerun":25,"daten":73,"steal":1},
        {"year":2015,"name":"平田　良介","team":"(中)","dasuu":491,"hit":139,"homerun":13,"daten":53,"steal":11},
        {"year":2015,"name":"鳥谷　敬","team":"(神)","dasuu":551,"hit":155,"homerun":6,"daten":42,"steal":9},
        {"year":2015,"name":"福留　孝介","team":"(神)","dasuu":495,"hit":139,"homerun":20,"daten":76,"steal":1},
        {"year":2015,"name":"マートン","team":"(神)","dasuu":544,"hit":150,"homerun":9,"daten":59,"steal":0},
        {"year":2015,"name":"梶谷　隆幸","team":"(デ)","dasuu":520,"hit":143,"homerun":13,"daten":66,"steal":28},
        {"year":2015,"name":"新井　貴浩","team":"(広)","dasuu":426,"hit":117,"homerun":7,"daten":57,"steal":3},
        {"year":2015,"name":"田中　広輔","team":"(広)","dasuu":543,"hit":149,"homerun":8,"daten":45,"steal":6},
        {"year":2015,"name":"ゴメス","team":"(神)","dasuu":520,"hit":141,"homerun":17,"daten":72,"steal":0},
        {"year":2015,"name":"エルナンデス","team":"(中)","dasuu":498,"hit":135,"homerun":11,"daten":58,"steal":5},
        {"year":2015,"name":"雄平","team":"(ヤ)","dasuu":551,"hit":149,"homerun":8,"daten":60,"steal":7},
        {"year":2015,"name":"坂本　勇人","team":"(巨)","dasuu":479,"hit":129,"homerun":12,"daten":68,"steal":10},
        {"year":2015,"name":"畠山　和洋","team":"(ヤ)","dasuu":512,"hit":137,"homerun":26,"daten":105,"steal":0},
        {"year":2015,"name":"大島　洋平","team":"(中)","dasuu":565,"hit":147,"homerun":6,"daten":27,"steal":22},
        {"year":2015,"name":"バルディリス","team":"(デ)","dasuu":465,"hit":120,"homerun":13,"daten":56,"steal":0},
        {"year":2015,"name":"菊池　涼介","team":"(広)","dasuu":562,"hit":143,"homerun":8,"daten":32,"steal":19},
        {"year":2015,"name":"上本　博紀","team":"(神)","dasuu":375,"hit":95,"homerun":4,"daten":31,"steal":19},
        {"year":2015,"name":"長野　久義","team":"(巨)","dasuu":434,"hit":109,"homerun":15,"daten":52,"steal":3},
        {"year":2015,"name":"丸　佳浩","team":"(広)","dasuu":530,"hit":132,"homerun":19,"daten":63,"steal":15},
        {"year":2015,"name":"中村　悠平","team":"(ヤ)","dasuu":442,"hit":102,"homerun":2,"daten":33,"steal":3}
];
```

テストプログラムはこちら。2015年のチーム成績順に並べます。

```js
test1 = keywordsorter("team",['(ヤ)', '(巨)', '(神)', '(広)', '(中)', '(デ)'], npbScores);
console.log(test1);
```

結果はこうなりました。

```js
[ { year: 2015, name: '川端　慎吾', team: '(ヤ)', dasuu: 581, hit: 195, homerun: 8, daten: 57, steal: 4 },
 { year: 2015, name: '中村　悠平', team: '(ヤ)', dasuu: 442, hit: 102, homerun: 2, daten: 33, steal: 3 },
 { year: 2015, name: '畠山　和洋', team: '(ヤ)', dasuu: 512, hit: 137, homerun: 26, daten: 105, steal: 0 },
 { year: 2015, name: '雄平', team: '(ヤ)', dasuu: 551, hit: 149, homerun: 8, daten: 60, steal: 7 },
 { year: 2015, name: '山田　哲人', team: '(ヤ)', dasuu: 557, hit: 183, homerun: 38, daten: 100, steal: 34 },
 { year: 2015, name: '長野　久義', team: '(巨)', dasuu: 434, hit: 109, homerun: 15, daten: 52, steal: 3 },
 { year: 2015, name: '坂本　勇人', team: '(巨)', dasuu: 479, hit: 129, homerun: 12, daten: 68, steal: 10 },
 { year: 2015, name: 'ゴメス', team: '(神)', dasuu: 520, hit: 141, homerun: 17, daten: 72, steal: 0 },
 { year: 2015, name: 'マートン', team: '(神)', dasuu: 544, hit: 150, homerun: 9, daten: 59, steal: 0 },
 { year: 2015, name: '福留　孝介', team: '(神)', dasuu: 495, hit: 139, homerun: 20, daten: 76, steal: 1 },
 { year: 2015, name: '鳥谷　敬', team: '(神)', dasuu: 551, hit: 155, homerun: 6, daten: 42, steal: 9 },
 { year: 2015, name: '上本　博紀', team: '(神)', dasuu: 375, hit: 95, homerun: 4, daten: 31, steal: 19 },
 { year: 2015, name: '新井　貴浩', team: '(広)', dasuu: 426, hit: 117, homerun: 7, daten: 57, steal: 3 },
 { year: 2015, name: '菊池　涼介', team: '(広)', dasuu: 562, hit: 143, homerun: 8, daten: 32, steal: 19 },
 { year: 2015, name: '丸　佳浩', team: '(広)', dasuu: 530, hit: 132, homerun: 19, daten: 63, steal: 15 },
 { year: 2015, name: '田中　広輔', team: '(広)', dasuu: 543, hit: 149, homerun: 8, daten: 45, steal: 6 },
 { year: 2015, name: '平田　良介', team: '(中)', dasuu: 491, hit: 139, homerun: 13, daten: 53, steal: 11 },
 { year: 2015, name: '大島　洋平', team: '(中)', dasuu: 565, hit: 147, homerun: 6, daten: 27, steal: 22 },
 { year: 2015, name: 'エルナンデス', team: '(中)', dasuu: 498, hit: 135, homerun: 11, daten: 58, steal: 5 },
 { year: 2015, name: 'ルナ', team: '(中)', dasuu: 496, hit: 145, homerun: 8, daten: 60, steal: 11 },
 { year: 2015, name: 'バルディリス', team: '(デ)', dasuu: 465, hit: 120, homerun: 13, daten: 56, steal: 0 },
 { year: 2015, name: '筒香　嘉智', team: '(デ)', dasuu: 496, hit: 157, homerun: 24, daten: 93, steal: 0 },
 { year: 2015, name: 'ロペス', team: '(デ)', dasuu: 516, hit: 150, homerun: 25, daten: 73, steal: 1 },
 { year: 2015, name: '梶谷　隆幸', team: '(デ)', dasuu: 520, hit: 143, homerun: 13, daten: 66, steal: 28 } ]
```

指定したチーム順に選手成績がソートされました。
よく見てみると、同じチームの選手は元データの順序（打率順）とは異なるようです。結果から推測すると、おそらく２番目以降のデータが、ソート済みデータの１件目のデータの後ろに配置されるから、このようなデータになっているようです。

# まとめ
以上、javascriptで配列を指定したキーワード順にソートする方法でした。自分的にはindexOfでソート順を取得するところが気に入ってます。テーブル用データを、指定されたテーブルのレイアウト通りに並べる時などに使えますね。


# 追記：2015/06/19

たくさんのコメントを頂戴しましたので、頂いたコメントの内容を反映したバージョンを公開します。

```keywordsorter.js
// 指定したキーワード順に配列をソートする
keywordsorter = function(key,keywords,array) {
    // インデックスマップまたはオブジェクトを取得する
    var keyIndex = (function(keys) {
        // Map()使える
        if(typeof Map === 'function') {
            return keys.reduce(function(map,a,idx) {
                return map.set(a,idx);
            },new Map());
        }
        // Map()使えない
        return keys.reduce(function(obj,a,idx) {
            obj[a] = idx;
            return obj;
        },{});
    })(keywords);

    // ソート順を取得する
    var getSequenceNumber = function(a) {
        // Map()使える
        if(typeof Map === 'function') {
            var seq = keyIndex.get(a[key]);
            return (typeof seq !== 'undefined')  ? seq : array.length;
        }
        // Map()使えない: オブジェクト使う
        return (a[key] in keyIndex) ? keyIndex[a[key]] : array.length;
    };

    // ソート結果を返す
    return array.sort(function(a,b) {
        var seqA = getSequenceNumber(a);
        var seqB = getSequenceNumber(b);
        return seqA - seqB;
    });
};
```

主な変更点は下記の通りです。

- ソート結果を0,1,-1ではなく比較結果で返す
- 順序をArray.indexOf()を使わずにMap()またはオブジェクトから取得する（引数は配列のまま）

動作確認は node.js v6.2.1、オブジェクトでの処理はスクリプト先頭に`Map = null;`でMap()を上書きして実行しました。
また、Map()を使う処理と使わない処理を混在して記述していますので、ご利用の際は環境に合わせて適宜修正してください。