かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Object の入った配列を特定のキーでソートしてランキング付けしたい

こんなデータを特定のキーの値でソートしてランキングを付けたい

const idols = [
  {id: 1,  name: '星宮いちご',  score: 90},
  {id: 2,  name: '霧矢あおい',  score: 83},
  {id: 3,  name: '紫吹蘭',     score: 80},
  {id: 4,  name: '有栖川おとめ', score: 85},
  {id: 5,  name: '藤堂ユリカ',  score: 87},
  {id: 6,  name: '神崎美月',   score: 92},
  {id: 7,  name: '夏樹みくる',  score: 80},
  {id: 8,  name: '大空あかり',  score: 91},
  {id: 9,  name: '氷上すみれ',  score: 83},
  {id: 10, name: '新条ひなき',  score: 80},
];
作戦
  1. score でソートした配列を作る
  2. 元の配列をループで回して、ソートした配列で同じ id を持つアイテムの index + 1 が順位になる

1. 配列内のオブジェクトのキーでソートする

Array.sort() にソート順を定義する関数を渡してソートをコントロールする事ができる
e.g.

// ASC 昇順
arr.sort((a, b) => {
  if ( a === b ) { return 0; }
  return a < b ? -1 : 1;
});

// DESC 降順
arr.sort((a, b) => {
  if ( a === b ) { return 0; }
  return a > b ? -1 : 1;
});

cf. Array.prototype.sort() - JavaScript | MDN

オブジェクトのキーでソートする場合は、このソート関数の中をオブジェクトのキーで比較するようにすればOK

score キーでソートする関数

const sortByScore = (arr) => (orderBy = 'DESC') => {
  // array.sort は元の配列を変更してしまうのでコピーしたものを使用する
  return […arr].sort((a, b) => {
    if ( a.score === b.score ) { return 0; }
    
    if ( orderBy === 'ASC' ) {
      [a, b] = [b, a];
    }

    return a.score > b.score ? -1 : 1;
  });
};

console.log( sortByScore(idols)('DESC') );
/* 
[ { id: 6, name: '神崎美月', score: 92 },
  { id: 8, name: '大空あかり', score: 91 },
  { id: 1, name: '星宮いちご', score: 90 },
  { id: 5, name: '藤堂ユリカ', score: 87 },
  { id: 4, name: '有栖川おとめ', score: 85 },
  { id: 2, name: '霧矢あおい', score: 83 },
  { id: 9, name: '氷上すみれ', score: 83 },
  { id: 3, name: '紫吹蘭', score: 80 },
  { id: 7, name: '夏樹みくる', score: 80 },
  { id: 10, name: '新条ひなき', score: 80 } ]
*/

console.log( sortByScore(idols)('ASC') );
/* =>
[ { id: 3, name: '紫吹蘭', score: 80 },
  { id: 7, name: '夏樹みくる', score: 80 },
  { id: 10, name: '新条ひなき', score: 80 },
  { id: 2, name: '霧矢あおい', score: 83 },
  { id: 9, name: '氷上すみれ', score: 83 },
  { id: 4, name: '有栖川おとめ', score: 85 },
  { id: 5, name: '藤堂ユリカ', score: 87 },
  { id: 1, name: '星宮いちご', score: 90 },
  { id: 8, name: '大空あかり', score: 91 },
  { id: 6, name: '神崎美月', score: 92 } ]
*/

score が同じ場合は 0 が返るので常に index が先のものが先になるようになっています。
score が大きい順のときは id が早い方が先、小さい順のときは id が後の方が先にしたい場合は条件を次のように変更すればOKです。

const sortByScore = (arr) => (orderby = 'DESC') => {
  return [...arr].sort((a, b) => {
    if ( orderby === 'ASC' ) {
      [a, b] = [b, a];
    }

    return (a.score > b.score || a.score === b.score && a.id < b.id) ? -1 : 1;
  });
};

2. 元の配列をループしてソートした配列から順位を取り出す

2.1. ソートした配列を id だけのリストに変換する

score 順にソートした配列だと、内部にオブジェクトを持っているので、元配列の id が何番目か探すのが少し面倒です。
単純な Array.indexOf で順位を取れるように、ソートした配列を id だけの配列に変換します。(ユニークなキーであれば問題ないので name でも別に構わないですが)

const mapDataToKeyArray = (arr, key = 'id') => {
  return arr.map((item) => {
    return item[key];
  });
};

// score の大きい順に並べたリスト
const idolsOrderByScoreDESC = sortByScore(idols)('DESC');
// => [ { id: 6, name: '神崎美月', score: 92 }, { id: 8, name: '大空あかり', score: 91 }, … ]

// id だけの配列に変換
mapDataToKeyArray( idolsOrderByScoreDESC );
// => [ 6, 8, 1, 5, 4, 2, 9, 3, 7, 10 ]

// name だけの配列に変換
mapDataToKeyArray( idolsOrderByScoreDESC, 'name' );
// => [ '神崎美月', '大空あかり', '星宮いちご', '藤堂ユリカ', … ]

2.3. ソートしたデータを id だけに変換した配列の index から順位をつける

const mapRankingToData = (arr, key = 'id') => (sortedArr) => {
  const sortedKeys = mapDataToKeyArray(sortedArr, key);
  const ranking = arr.map((item) => {
    const id = item[key];
    // ランキングが無い場合: array.indexOf(id) =>-1, -1 + 1 => 0
    const rank = sortedKeys.indexOf(id) + 1;
    // 配列内のオブジェクトは参照値になっているので `…item` でコピーする
    return {...item, ranking: rank || null};
  });

  return ranking;
};

// score の大きい順に並べたリスト
const idolsOrderByScoreDESC = sortByScore(idols)('DESC');

// ランキングを追加したデータ
const rankedList = mapRankingToData(idols)(idolsOrderByScoreDESC);
/* =>
[ { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 2, name: '霧矢あおい', score: 83, ranking: 6 },
  { id: 3, name: '紫吹蘭', score: 80, ranking: 8 },
  { id: 4, name: '有栖川おとめ', score: 85, ranking: 5 },
  { id: 5, name: '藤堂ユリカ', score: 87, ranking: 4 },
  { id: 6, name: '神崎美月', score: 92, ranking: 1 },
  { id: 7, name: '夏樹みくる', score: 80, ranking: 9 },
  { id: 8, name: '大空あかり', score: 91, ranking: 2 },
  { id: 9, name: '氷上すみれ', score: 83, ranking: 7 },
  { id: 10, name: '新条ひなき', score: 80, ranking: 10 } ]
*/

まとめ

全体のコードをまとめるとこんな感じ

const sortByScore = (arr) => (orderby = 'DESC') => {
  return [...arr].sort((a, b) => {
    if ( orderby === 'ASC' ) {
      [a, b] = [b, a];
    }

    return (a.score > b.score || a.score === b.score && a.id < b.id) ? -1 : 1;
  });
};

const mapDataToKeyArray = (arr, key = 'id') => {
  return arr.map((item) => {
    return item[key];
  });
};

const mapRankingToData = (arr, key = 'id') => (sortedArr) => {
  const sortedKeys = mapDataToKeyArray(sortedArr, key);
  const ranking = arr.map((item) => {
    const id = item[key];
    const rank = sortedKeys.indexOf(id) + 1;
    return {...item, ranking: rank || null};
  });

  return ranking;
};

// ランキングを追加したデータ
const rankedList = mapRankingToData(idols)(sortByScore(idols)('DESC'));
/* =>
[ { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 2, name: '霧矢あおい', score: 83, ranking: 6 },
  { id: 3, name: '紫吹蘭', score: 80, ranking: 8 },
  { id: 4, name: '有栖川おとめ', score: 85, ranking: 5 },
  { id: 5, name: '藤堂ユリカ', score: 87, ranking: 4 },
  { id: 6, name: '神崎美月', score: 92, ranking: 1 },
  { id: 7, name: '夏樹みくる', score: 80, ranking: 9 },
  { id: 8, name: '大空あかり', score: 91, ranking: 2 },
  { id: 9, name: '氷上すみれ', score: 83, ranking: 7 },
  { id: 10, name: '新条ひなき', score: 80, ranking: 10 } ]
*/

関数を組み合わせる設計にしておけば、配列をfilter する関数を作成して score > 85 のランキングも簡単に作ることができる

const filterScore = (arr) => (limit) => {
  return arr.filter((item) => {
    return item.score > limit;
  });
};

// score > 85 だけのデータを抽出
const overScore85Idols = filterScore(idols)(85);
// score > 85 のランキングを追加しデータ
const rankedList = mapRankingToData(overScore85Idols)(sortByScore(overScore85Idols)('DESC'));
/* =>
[ { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 5, name: '藤堂ユリカ', score: 87, ranking: 4 },
  { id: 6, name: '神崎美月', score: 92, ranking: 1 },
  { id: 8, name: '大空あかり', score: 91, ranking: 2 } ]
*/

ランキングを付けてランキング順に並べたデータを作ることも簡単です

// score > 85 だけのデータを抽出
const overScore85Idols = filterScore(idols)(85);
// score が高い順に並べたリスト
const sortedIdols = sortByScore(overScore85Idols)('DESC')
// ランキングを追加したランキング順のリスト
const rankedList = mapRankingToData(sortedIdols)(sortedIdols);
/* =>
[ { id: 6, name: '神崎美月', score: 92, ranking: 1 },
  { id: 8, name: '大空あかり', score: 91, ranking: 2 },
  { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 5, name: '藤堂ユリカ', score: 87, ranking: 4 } ]
*/

関数楽しい!
何回も配列ぶん回してるから計算量は多そうだけど、疎結合?にするならこんな感じで良いのかな?


[参考]

アイカツオンパレード! Blu-ray BOX 1

アイカツオンパレード! Blu-ray BOX 1

  • 出版社/メーカー: Happinet
  • 発売日: 2020/04/02
  • メディア: Blu-ray

2020年もアイカツ!を見れば幸せになれる!