JavaScriptのsortメソッドを使って、永遠にただ1人のボカロPの曲をおすすめし続けるサイトを作った

この記事は、[クソアプリ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kuso-app)のシリーズ3の10日目の記事です。

僕、この前、コロナにかかったんですよ、（唐突）そして現在絶賛療養中でして、することと言ったらQiitaとTwitterとニコ動とにらめっこするくらいしかなく、暇でして。  
そこで、アドベントカレンダーのネタなにかないかと考えていたら、コロナになる前に書いてたとあるコードがあったので、それを題材として、JavaScriptのsortメソッドについて勉強して書きなおして投稿しようかと思います。  

まだまだ初心者の初心者ですので、拙い部分、色々あるかと思いますが、というか拙い部分だらけだと思いますが、優しい目で見ていただけるとありがたいです。いいねとかくださると今後の勉強の励みになります。  

あと，なんか間違いがあったらコメントで指摘していただけるとありがたいです．  

あとあと，布団の中でスマホで書いた記事を，コード修正の時にWindowsで編集して...とかやってるんで，IME設定がばらばらで句読点が「、。」と「，．」混在してるのは許してください．

# はじめに  
2022年10月頃、Twitterのボカロ界に、とあるサイトが知れ渡りました。ボカロ曲を1曲入力すると、おすすめが10曲表示されるWEBサイトです。↓  

https://vector.streamlit.app/

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ボカロ曲を1曲入力すると、おすすめが10曲表示されるWEBサイトを公開しました。500人以上の「ボカロ100選」を独自に分析した推薦エンジンなため、おすすめされる楽曲は全て誰かの「めちゃくちゃ好き」というフィルターを通過した良曲です。ぜひ試してみてください <a href="https://twitter.com/hashtag/%E6%8B%A1%E6%95%A3%E5%B8%8C%E6%9C%9B?src=hash&amp;ref_src=twsrc%5Etfw">#拡散希望</a><a href="https://t.co/2OKnGK54Vr">https://t.co/2OKnGK54Vr</a></p>&mdash; melonade (@melodynade) <a href="https://twitter.com/melodynade/status/1581208268680343552?ref_src=twsrc%5Etfw">October 15, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Pythonでかかれているようで、見た時感動しました。（小並感）  
僕の大好きなボカロP、Adeliae（アデリー）さん（以下、あでさんと表記）もそれを試してみたらしく、ツイートで反応があったりしたんですが、そのうちのひとつとして、サブ垢で以下のようなものがありました。  

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">好きなボカロ曲を一個入力すると、アデリーの曲を無条件で10曲返してくれるステマサイト</p>&mdash; あで (@xi_chiekoin) <a href="https://twitter.com/xi_chiekoin/status/1582355804724006913?ref_src=twsrc%5Etfw">October 18, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こりゃーつくるしかないやろ！  

:::note warn
この記事及び該当サイトの内容に関して、あでさんに直接問い合わせるのはお控えください。
:::

# 方針  
とは言っても、僕にはそんな高度な技術はありません。Pythonでベクター処理出来ないですし、機械学習もまだやったことないですし、実はPHPでサーバーサイド処理も出来な…待って逆に俺何ができるん？悲しくなってきた。  

学校の合間にゆるりとやってる[^1]からこうなります。今後はもっとしっかりコンピュータ弄りたいなと思った僕でした。  

…話が逸れた。サーバーサイド処理ができるサーバーを用意したところで管理が大変だし、JavaScriptで組めた方が僕の技術的にもネットワーク的にもありがたい[^2]ので、結局は全部クライアント側で完結させてしまいましょう。  

[^1]: 暇で暇で仕方がない英語の授業中とか  
[^2]: わざわざ入力した情報送らなくて済むんだよ？これは大革命（なんかすいません）

# 実装  
これです。

https://alpaca-honke.github.io/ade-music/

# 特徴  
- 通信はHTTPリクエストだけなので通信制限のかかりそうなスマホでも安心して実行できる[^3]  
- 元サイト（ダークテーマ）にできるだけデザインを寄せた（ここに関してはこの記事では扱いません）  
- 頑張った[^4]  

[^3]: POSTとかしたところで通信量なんて知れてるので気にしないでもらって大丈夫です…
[^4]: 英語の授業中、指名されてもちゃんと答えられるように準備しながら頑張って書いてました

# 使い方  
見たまんまです。入力ボックスに任意の文字を入力して、リターンキーもしくはOKボタンを押すと、入力ボックス以下にあでさんの動画が10個、ランダムに表示されます。  

# 説明  
ここでは、動画のランダム選出方法と、動画表示方法を解説します。それ以外は解説しません…というか解説するところがありません。  

## 動画のランダム選出方法（本題）  
### 以前まで実装されていた方法  
以前までは、このような方法で10曲選出していました。  
（変数の初期化で、まるで宣言をせずに変数取り扱っているように見えますけど、ちゃんとコードの冒頭で宣言してあります。）  
```javascript:ade-music.js
const videoid = ["sm37910958","sm38620406","sm38937833","sm39126100","sm39469505","sm39759354","sm39911812","sm40238726","sm40347559","sm40515900","sm40823885","sm41190526","sm41410666"];

num_tmp = 0;
result = true;
videonum = [];

while (videonum.length < 10){
	   num_tmp = Math.floor(Math.random() * 13);

	   result = videonum.some((value) => {
	       return value === num_tmp;
	   });

	if (result == false){
		videonum.push(num_tmp);
        //動作確認用
        //console.log(videonum);
	}
};
```

```videonum```配列に，10個のランダムな重複のない数字が格納されます．  

```videoid```配列には、ニコニコ動画の動画IDが保管されています。次項で詳しく説明します。  
この配列のindex番号からランダムで10個取り出せればランダムに10曲おすすめできるわけですね！  

手順としては、簡単に書くと以下の通りです。  
重複が発生しないように、いちいち重複を確認するというめちゃくちゃめんどくさい処理を行ってますね。  
1. 変数```num_tmp```に0から12（現時点での総楽曲数は13曲なので）までのランダムな数字を代入する  
1. 配列```videonum```の中に```num_tmp```に入ってる数字がなければ、```videonum```の末尾に追加、そうでなければループの先頭に戻る
1. 上記1,2を、```videonum```の長さが10になるまで繰り返す  

…これでも上手くいくんですよ、でも、いちいちこんなことしてたらめんどくさいじゃないですか。  
何より、ランダムに生成した数が```videonum```に既に含まれている…なんて事象が永遠に続いた場合、いつになっても処理が終わってくれません。  
それは「動作確認用」と着けられた部分のコメントアウトを外して動作確認すると一目瞭然ですね．ブラウザのコンソールに、```videonum```がいちいち出力されますが、最後の方になると場合によっては十数回、数十回と繰り返されます。  
コンピュータの負担も少しは考えろよ。  

ということで、ちょっと調べてみたんです。すると画期的な方法が見つかりました。（？）  

### 今回実装した方法  
```javascript:ade-music.js
//変数の初期化
	numbers = [0,1,2,3,4,5,6,7,8,9,10,11,12];
	shuffled_numbers = [];
	//ランダムに10個選出
	numbers.sort(() => 
		Math.random() - 0.5
	);

	shuffled_numbers = numbers.slice(0,10);
```

```shuffled_numbers```配列に，重複のない10個のランダムな数字が入ります．  
めっちゃすっきりしたぜっ☆  

:::note warn
**2022.12.19追記**  
コメントをいただきましたが、```sort()```に```Math.random()```を渡すのは、シャッフル結果が偏りやすいようです。  
詳しくは解決策とともに後述します。  
:::

#### sortメソッドって何？  
これを書きたかったのにここまでめっちゃ長くなっちゃった．  

sortメソッドは，配列の内容を特定のルールに従って並べ替えるためのメソッドです．  
```[配列].sort(ルールを指定する関数);```の構文で使用します．（この関数を比較関数という）  
例えば，文字をアルファベット順に並べ替えるのは簡単です．ルールを指定しなければアルファベット順になります．  
```javascript:
const arr = ["b","c","a"];
arr.sort(); //["a","b","c"]
```

ただ，これでは数字とかは並び変わりません．いや，正確には並び変わるんですが，数字も文字として処理されるので，一番左の数字の大小から優先して並べ変わってしまいます．めんどくさっ  

```javascript:
const arr = [3,2,15];
arr.sort(); //[15,2,3]
```

では，数字を並べ替えるには？以下のようにします．  

```javascript:
const arr = [3,2,15];
arr.sort((a,b) => a - b); //[2,3,15]
```

比較関数```a - b```は，aとbの値の大小によって出力が異なります．これを使って配列の内容を比較して並び替えているんです．  
具体的には，a>bの時は正，a<bの時は負が出力されますな．

では，今回の事例のようにランダムに並び替えるためには？以下のようにします．  
 
```javascript:
const arr = [1,2,3,4,5];
arr.sort(() =>
    Math.random() - 0.5
);
```

```Math.random()```を単体実行すると，0以上1未満のランダムな数字が発生します．そこから0.5を引くことで，正と負が出力される確率を1:1にして，完全に並べ替えがランダムになるようになっているのです．  

あとは，```[配列].slice(開始点 , 終了点)```で先頭の10項分抜き出してやれば，終わりですね．  

:::note warn
**2022.12.19追記**  
上述したように、```sort()```に```Math.random()```を渡すのは、シャッフル結果が偏りやすいようです。  
```sort()```は、前後の値を比較するために引数として渡された比較関数を使用します。  
その際に、配列の要素すべてを総当りで比較するのではなく、推移率（a < b ⋀ b < c ⇒ a < c などの関係）に基づいて並べ替えることになります。  

<details>
<summary>JavaScript風に書くとこうなるね（クリックで開く）</summary>

```
if (a < b && b < c){
　a < c になる
};
```

</details>

ここで、比較関数にランダムな要素が混じっていると、a < b ∧ b < cであってもc < aとなる可能性があり、推移律が破綻してしまいます。  
前から順に比較するだけなら問題ない（？）と思うのですが、正確に並べ替えるには総当たりで比較するか推移律を基に比較する必要があるため、ランダムな要素が含まれているとうまく並び替わらないのです。  

なお、参考ですが、Mozillaのドキュメントによると、比較関数は以下の条件を満たしている必要があります。  

> - 無害: 比較関数は比較されるオブジェクトや外部の状態を変更しません。（これは重要です。比較関数がいつ、どのように呼び出されるかは保証されていないので、特定の呼び出しが外部に見える影響を及ぼしてはいけません）。
> - 安定的: 比較関数は、同じ組の入力に対して常に同じ結果を返します。
> - 反射的: ```compareFn(a, a) === 0``` となります。
> - 対称的: ```compareFn(a, b)``` と ```compareFn(b, a)``` はともに ```0``` であるか、逆の符号でなければなりません。
> - 推移的: ```compareFn(a, b)``` と ```compareFn(b, c)``` がともに正、0、負のいずれかであれば、 ```compareFn(a, c)``` は前の 2 つと同じ符号になります。  
>
> [Array.prototype.sort() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#%E8%A7%A3%E8%AA%AC) より  

（```compareFn()```は```sort()```に渡される比較関数を指す）  

（ここまでの説明は、おそらくあっていますが間違っているかもしれない...。間違いや語弊などあればコメントでご指摘頂けるとありがたいです。）  
参考：[【JavaScript】arr.sort\(\(\) => Math.random\(\) – 0.5);がダメな理由の説明と適切なシャッフルの仕方](https://cpoint-lab.co.jp/article/202211/23527/) 

解決策として、以下のように該当箇所を修正しました。[フィッシャーイェーツのアルゴリズム](https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%83%E3%82%B7%E3%83%A3%E3%83%BC%E2%80%93%E3%82%A4%E3%82%A7%E3%83%BC%E3%83%84%E3%81%AE%E3%82%B7%E3%83%A3%E3%83%83%E3%83%95%E3%83%AB#%E6%94%B9%E8%89%AF%E3%81%95%E3%82%8C%E3%81%9F%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0)ですね。  

```javascript:
const shuffleArray = (array) => {
	for (i = array.length -1 ; i > 0 ; i--){
		j = Math.floor(Math.random() * (i + 1));
		[array[i], array[j]] = [array[j], array[i]];
	}
};


function enter(){
    (省略)
    //ランダムに10個選出
	shuffleArray(numbers);
	shuffled_numbers = numbers.slice(0,10);
    (省略)
}
```
なお、この記事は```sort()```について（僕が）軽く理解するための記事ですので、追記するに留めます。  

:::

## 動画表示方法  
これは簡単です．まずはニコニコ動画の，動画埋め込み用HTMLタグはどんなふうになっているのでしょうか？以下のタグは記事投稿時点でのあでさんの最新曲，「ハルシネイション・シザーハンズ」のタグです．  

```HTML:
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm41410666/script?w=640&h=360">
</script>
<noscript>
    <a href="https://www.nicovideo.jp/watch/sm41410666">
        ハルシネイション・シザーハンズ feat. IA / Adeliae
    </a>
</noscript>
```

こうなっています．  
まず，このサイトはJavaScriptの実行前提なので，```<noscript>```以下は不要ですね．（ちなみにJSがオンになっていない場合，バカでかい字でオンにしてくださいって言われます．）  

noscriptを消すと以下が残ります．  

```HTML:
<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm41410666/script?w=640&h=360">
</script>
```

```src```属性には埋め込み用のURLが入っています．その中に```sm41410666```ってのが見えるでしょう？これが前述した動画IDですね．簡単に言うと動画URLの末尾です．  

ちなみに，僕が知ってる中で一番短いのは，```sm9```です．ニコニコに現存する最古の動画，伝説の「レッツゴー陰陽師」です．（どうでもよっ）

動画の識別子を書き換えれば表示される動画が変わるわけです．ですので，前項で生成したランダムな10個の数字を使って，コードの先頭に書いた配列  

<details>
<summary>
videoid（再掲，クリックで展開）
</summary>

```javascript
const videoid = ["sm37910958","sm38620406","sm38937833","sm39126100","sm39469505","sm39759354","sm39911812","sm40238726","sm40347559","sm40515900","sm40823885","sm41190526","sm41410666"];
```

</details>

から動画IDを抜き出してタグに引っ付ければおしまいですね．  

# 今後の課題・展望    
- 入力された文字がボカロ曲かどうか，スクレイピングかなんかで判定する  
- ちゃんとPHPとかPythonとかでこういうのつくれるようになる
- コロナ治す（←ここ重要）

# おわり  
初心者ながら頑張って書きました．いろんな意味で．  
読んでくれてありがとうございます．  
まずはコロナ治します．  
あと，あでさんの曲ここでおすすめしておきます．  

https://alpaca-honke.github.io/ade-music/

ここで一番上に出てきた曲，まず聴いてみてください！（地味に自己主張強いやつはここにいます）  

2022.12.20  
皆さん、色々とご指摘ありがとうございます。  
僕が誤解している点など、自分では気づけなかったりするので、助かります…