かもメモ

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

続・オブジェクトの入った配列からランキングをつくりたい

前回までのあらすじ、オブジェクト内の特定のキーから一意なランキングを作成してみました。

前回のコードでは、同じ得点の場合、配列の先にある方がランキングが上になるようにしていましたが、同位なランキングにしたい場合もあるな〜と思ったのでやってみました。

同位を許容する場合2通りの方法が考えられます

  1. 同位を許容して連続した順位付けをする
    1位 A, 2位 B, C, 3位 D … の様なランキング
  2. 同位があると同位の数分、次の順位をスキップする
    1位 A, 2位 B, C, 4位 D … の様なランキング

data
前回と同じ様なデータを使って scoreアイカツ!ランキングを作成します。

const idols = [
  {id: 1,  name: '星宮いちご',  score: 90},
  {id: 2,  name: '霧矢あおい',  score: 85},
  {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: 92},
  {id: 9,  name: '氷上すみれ',  score: 83},
  {id: 10, name: '新条ひなき',  score: 80},
];

共通の処理

データを特定のキーの順番に並べた配列を返したりする部分は前回と同様の関数を共通して使い回すことができます。(この処理のために最適化させることもできますが)

// `score` 順にソートした配列を返す関数
const sortByScoreAndID = (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;
  });
};

1. 同位を許可して連続した順位になるランキング

ランキングをマッピングするために次のようなデータ形式の配列を作成して、配列のインデックスから順位を作成すれば良さそうです

[
  [A],
  [B, C],
  [D],
  // …
];

1-1. ランキングをマッピング用の配列を返す関数

const mapDataToRankingArray = (arr, id = 'id') => (key = 'score') => {
  return arr.reduce((accumulator, item, index, _list) => {
    if ( index > 0 ) {
      const prevValue = _list[index - 1][key];
      // 1つ前の要素の `key` の値が同じなら同じ配列に入れる
      if ( prevValue === item[key] ) {
        accumulator[accumulator.length - 1].push(item[id]);
        return accumulator;
      }
    }
    accumulator[accumulator.length] = [item[id]];
    return accumulator;
  }, []);
};

const sorted = sortByScoreAndID(idols)('DESC');
const mapedRanking = mapDataToRankingArray(idols, 'name')();
/* => 
[ [ '神崎美月', '大空あかり' ],
  [ '星宮いちご' ],
  [ '藤堂ユリカ' ],
  [ '霧矢あおい', '有栖川おとめ' ],
  [ '氷上すみれ' ],
  [ '紫吹蘭', '夏樹みくる', '新条ひなき' ] ]
*/

1-2. マッピングされたデータからインデックスを取得する関数

Array.finfIndex を使って該当するデータがあれば、順位に使う配列のインデックスを返す関数を作成します

const getIndexFromRankArray = (arr) => (id) => {
  return arr.findIndex((keys) => {
    return keys.some((_id) => {
      return _id === id;
    });
  });
};

// mapedRanking 配列から '藤堂ユリカ' のあるインデックスを探して返す
console.log( getIndexFromRankArray(mapedRanking)('藤堂ユリカ') );
// => 2

// データに存在しない値を探そうとした場合 -1 が返る (ランキングなし)
console.log( getIndexFromRankArray(mapedRanking)('ジョニー別府') );
// => -1 

1-3. 元の配列をループしてランキングをマッピングする

後は作成した関数を利用して元のデータにランキングをマッピングするだけです

const addRankingAllowSameRank = (list, id = 'id') => (sorted, key = 'score') => {
  sorted = sorted || list;
  // ランキング用に変換した配列
  const rankingList = mapDataToRankingArray(sorted, id)(key);
  return list.map((item) => {
    // ランキング要配列から値のあるインデックスを取得、ランキング用に +1 する
    const ranking = getIndexFromRankArray(rankingList)(item[id]) + 1;
    // ranking が 0 はランキングの配列にない場合 (-1) なのでランキングを付けない
    return {...item, ranking: ranking || null};
  });
};

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

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

👍

2. 同位があると同位の数分、次の順位をスキップする

次のようなスキップする空配列が入った配列を作成すれば良さそう

[
  [A],
  [B, C, D],
  [],
  [],
  [E],
  // …
];

2-1. ランキングをマッピング用の配列を返す関数

同位の場合は、同じ値のある配列にキーを入れて空配列を末尾に追加すれば良さそうです。

// マッピングされたデータからインデックスを取得する関数
const getIndexFromRankArray = (arr) => (id) => {
  return arr.findIndex((keys) => {
    return keys.some((_id) => {
      return _id === id;
    });
  });
};

const mapDataToRankingArrayWithSkip = (arr, id = 'id') => (key = 'score') => {
  return arr.reduce((accumulator, item, index, _list) => {
    if ( index > 0 ) {
      const prevValue = _list[index - 1][key];
      // 1つ前の要素の `key` の値が同じなら同じ配列に入れる
      if ( prevValue === item[key] ) {
        const prevID = _list[index - 1][id];
        // 同じ値の要素が入っているインデックスを取得する
        const sameRankIndex = getIndexFromRankArray(accumulator)(prevID);
        accumulator[sameRankIndex].push(item[id]);
        // 末尾に空配列を追加する
        accumulator.push([]);
        return accumulator;
      }
    }
    accumulator[accumulator.length] = [item[id]];
    return accumulator;
  }, []);
};

const sorted = sortByScoreAndID(idols)('DESC');
const mapedRanking = mapDataToRankingArrayWithSkip(idols, 'name')();
/* =>
[ [ '神崎美月', '大空あかり' ],
  [],
  [ '星宮いちご' ],
  [ '藤堂ユリカ' ],
  [ '霧矢あおい', '有栖川おとめ' ],
  [],
  [ '氷上すみれ' ],
  [ '紫吹蘭', '夏樹みくる', '新条ひなき' ],
  [],
  [] ]
*/

同値の場合、空配列が追加されるので、必ずしも1つ前のインデックスにある配列に値を追加すれば良い訳ではないので、1-2. で作成した「マッピングされたデータからインデックスを取得する関数」を利用して同じ値の入っている配列があるインデックスを取得してキーを追加するようにしています。

2-2. マッピングされたデータからインデックスを取得する関数を改修する

この場合もランキングの番号を生成するためにランキング用にフォーマットしたデータからインデックスを取得する関数を使います。
関数自体は先に定義したものを使い回せますが、空配列の場合も Array.some で回すのは少しもったいないので、空配列の場合即時 false を返すように関数を改修します。

const getIndexFromRankArray = (arr) => (id) => {
  return arr.findIndex((keys) => {
    // 配列でなかったり、から配列の場合は即時 false を返す
    if ( !Array.isArray(keys) || !keys.length ) {
      return false;
    }

    return keys.some((_id) => {
      return _id === id;
    });
  });
};

// mapedRanking 配列から '星宮いちご' のあるインデックスを探して返す
console.log( getIndexFromRankArray(mapedRanking)('星宮いちご') );
// => 2 ※第2位(index: 1)がスキップされている

// データに存在しない値を探そうとした場合 -1 が返る (ランキングなし)
console.log( getIndexFromRankArray(mapedRanking)('星宮らいち') );
// => -1 

2-3. 元の配列をループしてランキングをマッピングする

1-3. との違いは ランキング用の配列を作る関数が異なるだけで他は同じです。

const addRankingAllowSameRank = (list, id = 'id') => (sorted, key = 'score') => {
  sorted = sorted || list;
  // ランキング用に変換した配列 (同値がある分次の順位をスキップ)
  const rankingList = mapDataToRankingArrayWithSkip(sorted, id)(key);
  return list.map((item) => {
    const ranking = getIndexFromRankArray(rankingList)(item[id]) + 1;
    return {...item, ranking: ranking || null};
  });
};

const sorted = sortByScoreAndID(idols)('DESC');
addRankingAllowSameRank(idols)(sorted);
/* => 
[ { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 2, name: '霧矢あおい', score: 85, ranking: 5 },
  { 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: 8 },
  { id: 8, name: '大空あかり', score: 92, ranking: 1 },
  { id: 9, name: '氷上すみれ', score: 83, ranking: 7 },
  { id: 10, name: '新条ひなき', score: 80, ranking: 8 } ]
*/

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

👍

まとめ

ランキングを生成するための関数が異なるだけなので、オプションで変更できるようにしてまとめるとこんな感じ

// `score` 順にソートした配列を返す関数
const sortByScoreAndID = (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;
  });
};

// マッピングされたデータから id のあるインデックスを返す関数
const getIndexFromRankArray = (arr) => (id) => {
  return arr.findIndex((keys) => {
    if ( !Array.isArray(keys) || !keys.length ) {
      return false;
    }

    return keys.some((_id) => {
      return _id === id;
    });
  });
};

// key が同じ値なら同位になるランキングの配列を返す関数
const mapDataToRankingArray = (arr, id = 'id') => (key = 'score') => {
  return arr.reduce((accumulator, item, index, _list) => {
    if ( index > 0 ) {
      const prevValue = _list[index - 1][key];
      if ( prevValue === item[key] ) {
        accumulator[accumulator.length - 1].push(item[id]);
        return accumulator;
      }
    }
    accumulator[accumulator.length] = [item[id]];
    return accumulator;
  }, []);
};

// key が同じ値なら同位になり次の順位をスキップしたランキングの配列を返す関数
const mapDataToRankingArrayWithSkip = (arr, id = 'id') => (key = 'score') => {
  return arr.reduce((accumulator, item, index, _list) => {
    if ( index > 0 ) {
      const prevValue = _list[index - 1][key];
      if ( prevValue === item[key] ) {
        const prevID = _list[index - 1][id];
        const sameRankIndex = getIndexFromRankArray(accumulator)(prevID);
        accumulator[sameRankIndex].push(item[id]);
        accumulator.push([]);
        return accumulator;
      }
    }
    accumulator[accumulator.length] = [item[id]];
    return accumulator;
  }, []);
};

// 同位になるランキングを付けた配列をを返す関数
const addRankingAllowSameRank = (list, id = 'id') => (sorted, key = 'score') => (skipRank = true) => {
  sorted = sorted || list;
  const rankingList = skipRank
    ? mapDataToRankingArrayWithSkip(sorted, id)(key)
    : mapDataToRankingArray(sorted, id)(key);

  return list.map((item) => {
    const ranking = getIndexFromRankArray(rankingList)(item[id]) + 1;
    return {...item, ranking: ranking || null};
  });
};

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

// 同位あり・順位スキップありのランキング
addRankingAllowSameRank(idols)(idolsOrderByScoreDESC)(true)
/* =>
[ { id: 1, name: '星宮いちご', score: 90, ranking: 3 },
  { id: 2, name: '霧矢あおい', score: 85, ranking: 5 },
  { 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: 8 },
  { id: 8, name: '大空あかり', score: 92, ranking: 1 },
  { id: 9, name: '氷上すみれ', score: 83, ranking: 7 },
  { id: 10, name: '新条ひなき', score: 80, ranking: 8 } ]
*/

👌👌👌
これでアイカツ!ランキング作りはバッチリ!
アイカツ!エンジニアに一歩近づけましたね🌟


[参考]

関数型プログラミングの基礎 JavaScriptを使って学ぶ

関数型プログラミングの基礎 JavaScriptを使って学ぶ

アイカツ!…みた?