1～7の数字から、ランダムで3個取り出したい（練習）

titleの通り、「1～7の数字から、ランダムで3個取り出したい」と考えています。色々な方法が考えられるとは思うのですが、では具体的にはどういったコードが考えられるでしょうか？

有りそうなものから無さそうなものまで、色々な方法を考え、その中から良さそうなコードをチョイス出来たらなと思っています。
## やりたいこと

- 1～7の数字から、ランダムに3個取り出したい
- 取り出した3個の数字は、配列に収める
- それぞれの数字は、Number型にする
- ランダム性に偏りが無い方法を重視したい
- 広い視点で方法を考えてみたい

### .sort(_ => Math.random() - 0.5) 型
有名な「.sort(_ => Math.random() - 0.5)」というシャッフルコード（名前はあるのかな？）を用いてみました。
~~~js
const sort_half = () => {
    const arr = [1, 2, 3, 4, 5, 6, 7];
    arr.sort(_ => Math.random() - 0.5);
	const result = arr.slice(0, 3);
		
    return result;
}
~~~

- まず、arr = [1,2,3,4,5,6,7] という配列を準備
- 配列を.sort(_ => Math.random() - 0.5)でシャッフル
- 配列の初めの3個を.slice(0, 3) で抽出

|	1~10	|	11~20	|	21~30	|
|	-	|	-	|	-	|
|	[3, 4, 5]	|	[7, 6, 5]	|	[4, 1, 3]	|
|	[6, 5, 7]	|	[4, 7, 2]	|	[6, 4, 5]	|
|	[1, 2, 7]	|	[6, 1, 7]	|	[1, 2, 3]	|
|	[7, 5, 6]	|	[6, 5, 4]	|	[7, 1, 2]	|
|	[5, 3, 6]	|	[6, 4, 3]	|	[7, 6, 4]	|
|	[4, 6, 3]	|	[7, 3, 4]	|	[3, 6, 2]	|
|	[2, 7, 1]	|	[5, 4, 1]	|	[4, 5, 6]	|
|	[4, 6, 1]	|	[1, 2, 5]	|	[7, 1, 2]	|
|	[5, 6, 1]	|	[1, 5, 2]	|	[7, 4, 5]	|
|	[6, 1, 7]	|	[4, 2, 3]	|	[1, 7, 2]	|

適当に30回ほど回して、データを取ってみました。
初期値が決まっているので、結果にある程度偏りが出ると思っていましたが、意外にもバラけている印象。

何度か試験で回してみて、気に入らないところがあれば適宜改良を加えていくといったところでしょうか？

### ダステンフェルド-亜型
配列のシャッフルで有名なものに、ダステンフェルドの手法というのがあります。少しばかり今風にアレンジを加えて、自分なりに使いやすい様にしてみました。

- まず、arr = [1,2,3,4,5,6,7] という配列を準備
- 配列をダステンフェルト-亜型でシャッフル
- 配列の初めの3個を.slice(0, 3) で抽出
~~~js
const shu_each = () => {
    const arr = [1, 2, 3, 4, 5, 6, 7];
	
	arr.forEach((_, i) => {
        let a = Math.trunc(Math.random() * (7 - i) + i);
        [arr[i], arr[a]] = [arr[a], arr[i]];
    });
		
	const result = arr.slice(0, 3);
		
    return result;
}
~~~
|	1～10	|	11～20	|	21～30	|
|	-	|	-	|	-	|
|	[5, 7, 4]	|	[5, 4, 6]	|	[2, 1, 6]	|
|	[6, 2, 4]	|	[1, 2, 6]	|	[7, 1, 3]	|
|	[6, 2, 3]	|	[4, 6, 2]	|	[1, 3, 5]	|
|	[4, 6, 5]	|	[2, 7, 4]	|	[2, 1, 5]	|
|	[2, 3, 5]	|	[1, 6, 7]	|	[1, 5, 7]	|
|	[6, 4, 2]	|	[3, 1, 4]	|	[2, 5, 1]	|
|	[5, 2, 7]	|	[3, 7, 5]	|	[3, 7, 6]	|
|	[5, 7, 4]	|	[6, 3, 7]	|	[6, 1, 3]	|
|	[6, 4, 2]	|	[3, 2, 4]	|	[6, 7, 4]	|
|	[3, 6, 2]	|	[7, 6, 2]	|	[1, 4, 5]	|

ランダム性においては、初期配列の先頭や末尾の数字（ここでは1と7）が頻出していないか？を見たりしているのですが、上手くシャッフルされているようで、とりあえず一安心ではあります。

### 正規表現.match()抽出型
単に3つの数値を取り出せばいいだけなんだから、わざわざシャッフルさせなくてもいいよね？的な思いでコードを作成してみました。
- 桁数の多いランダムな数字を一つ出す
- 1桁ずつ配列に格納、ついでに使わない数字は除去
- 出来た配列から重複をなくす
- .slice(0, 3)して、要素数3以上なら採用

~~~js
const ran_num = () => {
    let str, result;
	
    do{
        str = Math.trunc(Math.random() * 9e6 + 1e6)
            .toString()
			.match(/[1-7]/g);
			
		result = Array.from(new Set(str));
			
    } while(result.length < 3);
	
    return result.slice(0, 3).map(Number);
}
~~~

|	1~10	|	11~20	|	21~30	|
|	-	|	-	|	-	|
|	[7, 2, 4]	|	[3, 2, 7]	|	[4, 3, 5]	|
|	[7, 3, 2]	|	[5, 3, 2]	|	[7, 1, 2]	|
|	[5, 2, 7]	|	[3, 5, 6]	|	[5, 7, 2]	|
|	[1, 5, 2]	|	[1, 4, 3]	|	[6, 3, 5]	|
|	[6, 5, 2]	|	[3, 6, 2]	|	[6, 5, 7]	|
|	[1, 2, 4]	|	[1, 3, 6]	|	[3, 4, 7]	|
|	[7, 4, 3]	|	[6, 1, 7]	|	[4, 6, 7]	|
|	[1, 7, 4]	|	[3, 2, 4]	|	[3, 7, 5]	|
|	[7, 4, 6]	|	[5, 4, 2]	|	[5, 3, 4]	|
|	[5, 6, 7]	|	[3, 7, 4]	|	[6, 4, 3]	|

上手く抽出出来てますよね…？
初めのランダムな数値は10進法なんですが、途中の文字列化の所で.toString(7)や.toString(8)などとして、取り扱う数値を1～7に寄せる方法も試してみたんですが、あまりピンとは来なかったなあ…

### ランダムな数値ｎ個生成型（6個）
あまり小難しい事は考えず、1～7の範囲でランダムな数字をｎ個作って重複を取れば、それっぽい事出来るよね？的な大雑把な発想のもと、コードを作成してみました。
- 要素数6の配列を作成
- 1～7の範囲の数値をランダム生成して収める
- 重複を取って要素数3以上なら採用、.slice(0, 3)で抽出

~~~js
const ran_6 = () => {
    let arr, result;
	
    do {
        arr = [...Array(6)]
             .map(_ => Math.ceil(Math.random() * 7));
			 
        result = Array.from(new Set(arr));
			 
        } while(result.length < 3);

     return result.slice(0, 3);
}
~~~
|	1~10	|	11~20	|	21~30	|
|	-	|	-	|	-	|
|	[6, 3, 4]	|	[2, 1, 4]	|	[3, 6, 2]	|
|	[1, 3, 7]	|	[6, 1, 3]	|	[5, 6, 3]	|
|	[1, 5, 7]	|	[5, 1, 7]	|	[4, 5, 7]	|
|	[1, 7, 3]	|	[3, 5, 7]	|	[6, 1, 2]	|
|	[6, 2, 3]	|	[6, 5, 7]	|	[7, 6, 4]	|
|	[5, 6, 1]	|	[4, 2, 3]	|	[1, 7, 5]	|
|	[5, 7, 6]	|	[1, 4, 6]	|	[4, 2, 6]	|
|	[3, 1, 6]	|	[7, 3, 6]	|	[2, 5, 1]	|
|	[3, 4, 5]	|	[3, 2, 1]	|	[2, 4, 7]	|
|	[7, 6, 5]	|	[4, 2, 5]	|	[7, 5, 2]	|

始めの方に1,3,7　が続いているようにも見える…
こういうのは、変な偏りなのか、それとも想定の範囲内のものなのか…
何度か回してみて、こういった事が気になる様なら、少し改善を加えてみるのもいいかもしれませんね…

### perplexity先生に、お伺い型
『javascriptで、「1から7までの数字から、ランダムに3つを抜出すコード」を教えてください。ランダムに抜出した3つの数字は、配列に収めて下さい。』
[perplexity先生](https://www.perplexity.ai)にこのようなお伺いを立ててみたところ、ささっと提示してくれたコードが次のものになります。
~~~js
function getRandomNumbers() {
  // 1から7までの数字の配列を作成
  const numbers = [1, 2, 3, 4, 5, 6, 7];

  // 配列からランダムに3つの数字を選択
  const randomNumbers = [];
  for (let i = 0; i < 3; i++) {
    const randomIndex = Math.floor(Math.random() * numbers.length);
    randomNumbers.push(numbers[randomIndex]);
    numbers.splice(randomIndex, 1); // 選択した数字を配列から削除
  }

  return randomNumbers;
}

// 関数を呼び出して、ランダムに選択された3つの数字を取得
const result = getRandomNumbers();
console.log(result); // 例: [4, 2, 6]
~~~

こんな注釈付きでコードを速攻で作ってくれるなんて…
最近の検索機能ってすごいんですね(笑)
上記コードの関数名、変数名など、多少の改変を加え、試験してみます。

|	1~10	|	11~20	|	21~30	|
|	-	|	-	|	-	|
|	[4, 2, 6]	|	[3, 6, 5]	|	[3, 4, 7]	|
|	[6, 4, 1]	|	[5, 3, 6]	|	[2, 4, 6]	|
|	[4, 2, 6]	|	[4, 5, 1]	|	[4, 3, 2]	|
|	[7, 3, 4]	|	[7, 6, 5]	|	[2, 1, 5]	|
|	[1, 3, 5]	|	[2, 7, 4]	|	[1, 3, 7]	|
|	[2, 7, 3]	|	[4, 6, 3]	|	[1, 5, 4]	|
|	[3, 5, 1]	|	[3, 1, 2]	|	[6, 3, 4]	|
|	[5, 6, 1]	|	[3, 7, 5]	|	[6, 4, 3]	|
|	[7, 5, 1]	|	[1, 5, 2]	|	[7, 1, 2]	|
|	[7, 6, 3]	|	[1, 6, 3]	|	[7, 4, 5]	|

部分的にちょっと数字が連続してる様な感じも無いわけでもないけど、でも流石はperplexity先生ですね！

### new Set型
new Setの解説をざっと見てみると、
- 重複した値は入れられない
- 入れた順番が守られる

と、まるで今回やりたい事の為に用意されているかのような機能ですね。ちょっとこれでコードを作ってみます。

~~~js
const set_add = () => {
    const set = new Set();
	
	do {
	    set.add(Math.ceil(Math.random() * 7));
	} while(set.size < 3);
	
	const result = Array.from(set);
	
	return result;
}
~~~

|	1~10	|	11~20	|	21~30	|
|	-	|	-	|	-	|
|	[3, 4, 5]	|	[7, 2, 4]	|	[1, 5, 6]	|
|	[7, 1, 4]	|	[2, 3, 6]	|	[7, 2, 3]	|
|	[5, 3, 4]	|	[2, 1, 4]	|	[2, 3, 4]	|
|	[2, 1, 7]	|	[6, 3, 1]	|	[7, 4, 6]	|
|	[1, 4, 3]	|	[2, 5, 1]	|	[5, 6, 1]	|
|	[1, 2, 7]	|	[3, 6, 7]	|	[1, 6, 3]	|
|	[5, 1, 3]	|	[2, 4, 5]	|	[6, 2, 1]	|
|	[2, 4, 3]	|	[4, 1, 3]	|	[3, 1, 7]	|
|	[6, 7, 4]	|	[1, 2, 6]	|	[6, 5, 2]	|
|	[3, 7, 1]	|	[1, 5, 4]	|	[4, 1, 5]	|

ふむ、特に問題なさそうですね。
new Set new Map の関連ってなんかとっつきにくそうなイメージでなんとなく避けていたんですが、こうやってみるととても使いやすいですね～。
## タイム計測

ついでと言ってはなんですが、それぞれの関数をｎ回まわして、タイム計測をしてみようと思います。

~~~js:関数のタイム計測
const n = 1000;

const funcPerformance = (func, n) => {
    const startTime = performance.now();
	for (let i = n; --i;) {
        func ();
	}
    const endTime = performance.now();
    console.log(`「${func.name}」タイム ＝ ${((endTime - startTime) / 1000).toFixed(4)} 秒　回転 ＝ ${n}`);
}

funcPerformance(sort_half, n);
funcPerformance(shu_each, n);
funcPerformance(ran_num, n);
funcPerformance(ran_6, n);
funcPerformance(perp_lex, n);
funcPerformance(set_add, n);

~~~

### 1000回転（単位：秒）
表1
|	sort_half	|	shu_each	|	ran_num	|
|	-	|	-	|	-	|
|	0.0059	|	0.0057	|	0.0041	|
|	0.0043	|	0.0054	|	0.0031	|
|	0.0055	|	0.0051	|	0.0039	|
|	0.0059	|	0.0073	|	0.0043	|
|	0.0062	|	0.0078	|	0.0041	|


表2
|	ran_6	|	perp_lex	|	set_add	|
|	-	|	-	|	-	|
|	0.0042	|	0.0027	|	0.0025	|
|	0.0041	|	0.0021	|	0.0019	|
|	0.0026	|	0.0017	|	0.0026	|
|	0.0037	|	0.0027	|	0.0041	|
|	0.0031	|	0.0041	|	0.0027	|

1000回転程度では、あまり差が出るわけでもないですが…
順位をつけるなら、こんな感じでしょうか？
- perp_lex & set_add > ran_num & ran_6 > sort_half & shu_each

強いて挙げるなら、sort()系やシャッフル系が、少し手間取っている印象でしょうか？

※ 各自環境により、結果が変わることに留意

### 100万回転（単位：秒）
表1
|	sort_half	|	shu_each	|	ran_num	|
|	-	|	-	|	-	|
|	2.3431	|	0.7513	|	2.6381	|
|	2.3281	|	0.7834	|	2.7563	|
|	2.3983	|	0.7301	|	2.7521	|
|	2.3576	|	0.7206	|	2.5248	|
|	2.3818	|	0.7722	|	2.5233	|

表2
|	ran_6	|	perp_lex	|	set_add	|
|	-	|	-	|	-	|
|	1.7449	|	0.8403	|	0.5489	|
|	1.7458	|	0.8301	|	0.5288	|
|	1.7797	|	0.8184	|	0.5456	|
|	1.6692	|	0.8373	|	0.5301	|
|	1.7133	|	0.7733	|	0.5135	|

- set_add > shu_each & perp_lex > ran_6 > sort_half > ran_num

ん？1000回転とは少し違った結果がでましたねぇ。
少回数では成績が振るわなかった shu_each 型が、多回転では悪くない結果に。配列メソッドが何かカギを握ているんでしょうか?面白いですね。

※ 各自環境により、結果が変わることに留意

## コメント紹介コード
コメントでコードの案を戴いておりますので、ここで紹介させていただきたいと思います。

### perplexity先生お伺い、短縮ワンライナー
以下コードは、@ffngud588 さんよりコメントで頂いたコードになります。

~~~js
const f = (a = [1, 2, 3, 4, 5, 6, 7]) => [...Array(3)].map(() => a.splice(Math.random() * a.length, 1)[0]);

for (let i = 0; i < 10; i++) console.log(f());
/*
  [1, 3, 5]
  [5, 2, 6]
  [1, 4, 2]
  [2, 5, 3]
  [6, 7, 5]
  [2, 3, 7]
  [5, 1, 6]
  [4, 5, 2]
  [6, 5, 3]
  [5, 7, 1]
*/
~~~

「perplexity先生お伺い型が少し冗長気味だったので、簡潔にまとめました」という感じとお見受けしましたが、いかがでしょうか？使っているテクニックとしては、

- 関数の引数にデフォルト値を設定
- spliceの戻り値は、削除された値
- spliceの引数が整数じゃなくても動いている

spliceは少し苦手で、まだよくわからなかったのですが、今回ご紹介いただいた事で大変勉強になりました。ありがとうございました。（タイム計測は割愛）

## まとめ
- 所詮は単回転。100万回転もさせないんだから、好きなヤツを使えばよい。
- それでも選ぶなら、タイム計測で優秀、且つ見た目もスッキリな「new Set型」がおすすめ。
- ランダム性が気になるなら、2重回転や、2種回転などをを組み合わせても良いと思う。

それでは、
- こんなのあるよ
- もっと簡単に書けるよ
- それ、違ってるよ

などありましたら、気軽にコメント下さいませ。

### コード各種

~~~js:タイム計測用コード
const n = 1000;

const funcPerformance = (func, n) => {
    const startTime = performance.now();
	for (let i = n; --i;) {
        func ();
	}
    const endTime = performance.now();
    console.log(`「${func.name}」タイム ＝ ${((endTime - startTime) / 1000).toFixed(4)} 秒　回転 ＝ ${n}`);
}

// 各自環境により、結果が変わることに留意
~~~

~~~js:多回転結果ログ用コード
const r =30;

const cycle = (func,r) => {
    for(let i = r; i--;) {
	    console.log( func () );
	}
}
~~~

~~~js:シャッフルコード（ダステンフェルド）
function shuffle(arr) {
    let n = arr.length;
    let temp, i;
    while (n) {
      i = Math.floor(Math.random() * n--);
      temp = arr[n];
      arr[n] = arr[i];
      arr[i] = temp;
    }
    return arr;
  }
~~~


### 参考サイト

https://qiita.com/pure-adachi/items/77fdf665ff6e5ea22128

https://qiita.com/basictomonokai/items/44f734f603d8a206968f