かもメモ

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

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

  • 発売日: 2020/04/02
  • メディア: Blu-ray

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

React Hooks useState state の更新にハマる

useState の setter で値を更新しても即時 state に反映されるわけではないので、更新された state を使って別の処理を行いたい時などで意図しない動作になってしまうことがあります。

🐞

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = () => {
    setSquare( count * count );
  }

  const onCountup = () => {
    setCount( count + 1 );
    // ここではまだ count は更新されているとは限らない
    updateSquare();
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

👍

useState の setter で更新されたことを期待する state を使うのではなく、更新する値を別途変数化してそれを使ってそれぞれを更新すればOK

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = ( val ) => {
    setSquare( val * val );
  }

  const onCountup = () => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

👍👍Functional updates (高階関数)を使う

useState の setter に関数を渡す事ができ、この関数は前回の state を引数で受取り、返した値が新しい state になる
cf. フック API リファレンス 関数型の更新 – React

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = ( val ) => {
    setSquare( val * val );
  }

  const onCountup = () => {
    setCount( prevCount => {
      const newCount = prevCount + 1; 
      updateSquare( newCount )
      return newCount;
    } );
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

Functional updates (高階関数)を使うメリット

useCallback で関数の再生産を行わないようにする際に、高階関数を使う方法だと依存 (deps) を無くせるのでパフォーマンスを良くすることができる

🤔Functional updates (高階関数)でない方法の場合

Functional updates を使わない場合 state を deps に含める必要があるので、 state が変更される度に関数が再定義される

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // ...
  
  // 🤔 `count` が変更される度に `onCountup` は再定義される
  const onCountup = useCallback(() => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }, [count]);
 
  return ( ... );
}
🐞依存を与えないと逆にバグを発生させる

deps を空配列 [] にすれば依存はなくなり、関数の生成は1度になるが、関数内で使っている state が関数生成時の値でメモ化されてしまうので意図した動作にならない

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // …

  // 🐞 `count` は初期値 initCount で固定されしまう
  const onCountup = useCallback(() => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }, []);

  return ( ... );
}

👍👍👍Functional updates (高階関数)を使う方法の場合

Functional updates を利用すると関数外の変数を使わないので deps を無くせるので、関数の生成は1度で関数内の処理も意図したとおりに動作させることができる

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // …

  // 👍関数外の変数を使わないので関数再定義の依存がない
  const onCountup = useCallback(() => {
    setCount( prevCount => {
      const newCount = prevCount + 1; 
      updateSquare( newCount )
      return newCount;
    } ), []);
  }
 
  return ( ... );
}

sample

See the Pen React Hooks useState setter by KIKIKI (@chaika-design) on CodePen.

所感

Functional updates 知らなかった…
Array.map(function(val, key)) みないな感じなので、高階関数って呼んで良いんだと思うけどチョット自信ない。

codepen でサクット React Hooks も試せるから挙動確認みたいな時に便利ですね


[参考]

React Hooks コンポーネントのPropsにデフォルト値をつけたい

// Credit.js
export function Credit({ character, singer }) {
  return (
    <>
      <b>{character}</b>
      <small>song by: {singer}</small>
    </>
  );
} 

こんな感じのコンポーネントがあり、props を渡さなければデフォルトで character, singer を表示させたい

1. オブジェクトの引数を分割代入で取る時のデフォルト値の与え方を使う

👇の形を使う

function({ key = initValue } = {}) {...}

cf. JavaScript 分割代入な引数にデフォルト値を設定したい - かもメモ

// Credit.js
export function Credit({
  character = '大空あかり',
  singer = '遠藤瑠香'
} = {}) {
  return (
    <>
      <b>{character}</b>
      <small>song by: {singer}</small>
    </>
  );
}

// App.js
import { Credit } from './Credit';
function App() {
  return (
    <>
      <Credit />
      <Credit character="新条ひなき" singer="未来みき" />
      <Credit character="双葉アリア" />
    </>
  );
}

👇

<b>大空あかり</b><small>song by: 遠藤瑠香</small>
<b>新条ひなき</b><small>song by: 未来みき</small>
<b>双葉アリア</b><small>song by: 遠藤瑠香</small>

2. defaultProps プロパティを使う

公式ドキュメントには Class Components での使い方が ClassName.defaultProps = {...} と載っているだけだけど React Hooks ( Functional Components ) でも FunctionName.defaultProps = {...} で使えるっぽい

// Credit.js
export function Credit({ character, singer }) {
  return (
    <>
      <b>{character}</b>
      <small>song by: {singer}</small>
    </>
  );
}

// 先 export していても問題ないっぽい
Credit.defaultProps = {
  character: '大空あかり',
  singer: '遠藤瑠香',
};

// App.js
import { Credit } from './Credit';
function App() {
  return (
    <>
      <Credit />
      <Credit character="黒沢凛" singer="松岡ななせ" />
      <Credit character="双葉アリア" />
    </>
  );
}

👇

<b>大空あかり</b><small>song by: 遠藤瑠香</small>
<b>黒沢凛</b><small>song by: 松岡ななせ</small>
<b>双葉アリア</b><small>song by: 遠藤瑠香</small>

 
👍👍👍

sample

See the Pen React Hooks: default props example. by KIKIKI (@chaika-design) on CodePen.

所感

defaultProps を使うほうが関数定義の部分の見通しは良さそう。
ただ公式ドキュメントには Class Components のやり方しか載ってないみたいだったので、Functional Components でも動作はするけど使っても本当に大丈夫なのか少し謎 ( JavaScript の class 自体は中身は prototype 継承を利用した function だと思うので大丈夫だとは思うけど )


[参考]

灯りが照らす場所 (feat. 遠藤瑠香)

灯りが照らす場所 (feat. 遠藤瑠香)

  • アーティスト:connie
  • 出版社/メーカー: Fall Wait Records
  • 発売日: 2019/08/21
  • メディア: MP3 ダウンロード
大空あかりちゃんジャケット選挙1位おめでとうございます🎉