🐴

なんか数字を選んでくれるやつを作った

2024/07/28に公開

記事概要

MAXの数値を指定すると1からMAXの中からランダムに数字を選んでくれる、非常に簡素なツールをJavaScriptの勉強がてらに作りました。

※2024/08/09 Update
・CHOICEで選ぶ数を指定すると、1からMAXの中からランダムでCHOICEの数だけ数字を選んでくれるようになりました。

※2024/08/13 Update
・ランダムで選んだ数字が見やすくなりました。
・複数の数字を選ぶ場合、間に「>」が表示されるようになりました。

※2024/08/19 Update
・MAXの値に応じて、ランダムで選んだ数字に背景色がつくようになりました。

※2024/08/21 Update
・モードを追加しました。
 「たんたん」は順不同で数字を選んでくれます(既存の機能です)
 「ふくふく」は昇順で数字を選んでくれます(追加した機能です)

メジャーアップデートはこれで完了です。今後は気まぐれにブラッシュアップや機能追加をします。

想定される使用場面

・何か数字を選ばざるを得ない状況に直面した時
・あと1~3つ数字を選ぶとしたら何がいいかを機械的に選びたい時

筆者のスキル

HTML/CSS
10年ほどの業務経験。課題点について自分で調査&実装することができる。

JavaScript
10年ほど見ている。見ているだけ。本格的な開発では他の人のヘルプが必要。

GitHubページ

なんか数字を選んでくれるやつは、下記のGitHubページにて公開しています。
なんか数字を選んでくれるやつ

実装ポイント

本ツールは筆者のスキルに応じて段階的にアップデートしています。JavaScriptでの処理をメインに、実装にあたってのポイントをコードを抜粋して解説します。

LV.1 ランダムに数字を出力する

1からMAXの中から1つだけ出力する場合、下記のロジックで実装します。

  1. MAXの値を取得する
  2. MAXの値を元に選ぶ範囲を指定してランダムに数字を取得する
// ページを読み込む
window.onload = function () {
    // 「数字を選ぶ」を押下する
    document.getElementById("choose").onclick = function getRandom(){
        // MAXの値からランダムに数字を取得する
        max = document.getElementById("max").valueAsNumber;
        choice = Math.floor(Math.random() * (max - min + 1) + min);
        // 結果の出力
        document.getElementById('output').textContent = choice.toString();
    };
};

ランダムに取得する関数はMDN Web Docsより拝借しました。
Math.random()

LV.2-1 指定した数だけ、ランダムに重複なく数字を出力する

ロジックは大きく分けて3つです。まずはこの通りに実装し、細かく調整していきます。

最初にすること
・MAXとCHOICEの値を取得する
・MAXの値から、数字を選ぶベースとなる配列(番号配列と呼ぶことにします)を生成する

選ぶ数だけ繰り返すこと
・枠配列からランダムに数字を選んで、選ばれた数字を格納する配列(出力用配列と呼ぶことにします)に格納する
・番号配列から選ばれた要素を削除する

最後にすること
・出力用配列を出力する

1.MAXとCHOICEを取得する

IDがそれぞれ「max」「choice」のものを取得し、変数に格納します。

// MAXとCHOICEを取得する
max = document.getElementById("max").valueAsNumber;
choice = document.getElementById("choice").valueAsNumber;

2.番号配列を生成する

MAXの値から番号配列を生成します。
MDN Web Docsより、「連番の生成関数」を拝借しました。
Array.from()

// 関数
const range = (start, stop, step) =>
    Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);

この関数を利用して、取得したMAXの値を元に番号配列を生成しています。
引数スタートは配列の最初の数値、引数stopは配列の最後の数値、引数stepは連番の刻みの単位です。

例えばMAXが16の場合、1から16までの数字がそれぞれ格納された、長さが16の配列が生成されます。
本ツールでは変数minに1を代入しています。

// 番号配列の生成
maxArray = range(min,max,1);

3.ランダムに数字を取得する

出力用配列を作成します。
forループを回し、CHOICEで指定した数をデクリメントしつつ処理します。

// 数字の数だけランダムに数字を取得する
for(let i = choice; i > 0; i--){
    // 出力用配列に追加
    randomIndex = Math.floor(Math.random() * maxArray.length);
    choiceArray.push(maxArray[randomIndex]);
    // 配列の要素の削除
    maxArray.splice(randomIndex,1);
}

ここまで持ってくるまでありがちな失敗もあったので、forループの処理については1行ずつ説明します。
Math.random()関数を利用するのはLV.1と変わりないのですが、
・処理の中で変化する配列からランダムに取得する
・配列のインデックスをランダムの対象とする
という部分で苦慮しました。

配列のインデックスからランダムに数字を取得する、最終形態は下記のコードです。

randomIndex = Math.floor(Math.random() * maxArray.length);

当初として実装したのは下記のコードですが、想定した結果にそぐわない数値を引っ張ってきてしまい、undefinedが出力されることがあります。これではダメです。

randomIndex = Math.floor(Math.random() * maxArray.length - 1);

この実装に思い至った、ロジックという名の思い込みの過程は下記の通りです。

  1. 番号配列の長さを取得して、インデックスをランダムに取得する処理にしよう
  2. 仮にmaxArray.lengthが16ならランダムに取得される対象は1~16になるのでは?
  3. この場合、番号配列はインデックスが0~15になるので、Math.floor(Math.random() * maxArray.length)だと16を取得すると何かしらエラーになるのでは?
  4. だからmaxArray.length -1にしようかな
  5. あれ?こうなったらインデックスが0の要素を取得できない?

2.から思い込みが発生し、5.で正気に戻ります。
このロジックという名の思い込みを解消するため、実装にあたって参考になったサイトを記載します。
JavaScript で配列からランダムに抽出するコードをいま一度振り返るメモ

私は使おうとしていた関数の役割を十分に理解していませんでした。LV.1のランダムの処理についてもMDNからのコピペなので理解が進むはずがありません。わかったつもりになっていたのです。
思い込みの過程で出した例に戻って、今回の実装を整理します。maxArray.lengthが16とします。結果として0~15の整数を返すことになるので、インデックスをランダムに取得する実装としてはMath.floor(Math.random() * maxArray.length)で問題ないです。

  1. 0を含み、1未満浮動小数点の疑似乱数を返す
    →例の場合、0から16未満の浮動小数点を返します
  2. 1.で返された疑似乱数の小数点以下を切り捨てる 
    →例の場合、0~15の整数を返します

そうしてランダムに取得したインデックスの要素を出力用配列に格納します。

choiceArray.push(maxArray[randomIndex]);

次に選択する数字が重複しないよう、番号配列から現在のループで選択された要素を削除します。

maxArray.splice(randomIndex,1);

forループを回しきったら、出力用配列を文字列として出力します。配列をそのまま出力しているので、カンマを含みます。

// 結果の出力
document.getElementById('output').textContent = choiceArray.toString();

LV.2-2 入力した数字に応じてエラーを表示する

エラー用にdiv要素を追加して、CHOICEの値がMAX以上の場合テキストでエラーを表示します。
例えばMAXの値が2でCHOICEの値が2の場合、ツールとして意味を為さないのでエラーを表示します。

if(choice >= max){
            // CHOICEがMAX以上の場合、エラーを表示する
            document.getElementById('error').textContent = "CHOICEはMAXの数値より小さく指定してください。";
            document.getElementById('error').style.display = "block";
            document.getElementById('output').style.display = "none";
        }

LV.3-1 出力する数字をdiv要素にする


LV.2の状態では、ランダムに選んだ数字を配列として、清々しいグリーンを背景にそのまま出力しています。自己満足ならこれでいいのですが、個々の数字が見えづらく、ときめき?ロマン?そういうものが欠けていると感じていました。
では選んだ数字をもっと見やすくしましょう。具体的に言うなら、選んだ数字を配列として出力するのではなく、個々の独立した要素として出力するようにします。
アタリはついているけど理想形に持っていくまで何度もデバッグが必要な領域になってくるので、エディターと共に開発者ツールでコンソールを何度も呼び出すことになります。

清々しいグリーンの部分を取得します。何度かメソッドを実行することになるので、ここで変数に格納します。

outputElement = document.getElementById('output');

ランダムで取得した数字について、ここではdiv要素として出力することにします。forループの中に下記のコードを追加します。

// 取得した数字をdiv要素として出力
var outputchild = choiceArray.indexOf(choiceArray.at(-1));
choiceElement[outputchild] = document.createElement('div');
choiceElement[outputchild].style.width = "50px";
choiceElement[outputchild].innerHTML = choiceArray.at(-1);

outputElement.appendChild(choiceElement[outputchild]);

こちらも備忘録的に説明します。
まず「現在のループの中で扱っている数字をどうにかしたい」ので、at()メソッドを利用して、出力用配列の中の最後尾のインデックスを取得します。

var outputchild = choiceArray.indexOf(choiceArray.at(-1));

清々しいグリーンの部分の子要素として、div要素を作成していきます。幅を50pxにして、HTMLタグの中身を選択した数字にします。

choiceElement[outputchild] = document.createElement('div');
choiceElement[outputchild].style.width = "50px";
choiceElement[outputchild].innerHTML = choiceArray.at(-1);

上記を清々しいグリーンの部分の子要素として追加します。これを選択した数字の分だけ繰り返します。

outputElement.appendChild(choiceElement[outputchild]);

LV.3-2 数字の間に「>」を追加する

もとより数字を出力するだけのツールですが、カンマで間が区切られていると見えづらいです。カンマではない何か、数字の間に記号を……そう、例えば「>」とか入れてみたいと思いました。

// 選択する残りの数が1より大きい場合、>を追加する
if(i > 1){
    arrow = document.createElement('div');
    arrow.classList.add('arrow')
    arrow.innerHTML = ">";
    outputElement.appendChild(arrow);
}

forループの中に、数字を出力する処理の後に入れます。例えばCHOICEが3だったら、数字の間に「>」を合計2つ入れたいです。
LV.3-1の処理を参考に、arrowというclass名のdiv要素を作成して、清々しいグリーンの子要素として追加します。
今後の実装も考慮し、清々しいグリーンを一旦白にして確認します。

この状態で開発者ツールのコンソールでoutputElementと入力すると、div要素の追加の様子がわかりやすいですね。

LV.4 数字に背景色を入れる

もっとカラフルに、それでいて規則的に数字を出力したいと考えました。
おや、どうやら18を上限として色を規則的に振り分けることがこの世にはあるようです。偶然です。下記のページを参考に、背景色を入れる方法を考えます。

枠連とは? 出走馬を8つの『枠』に分ける馬券=枠連の特徴を紹介

背景色を入れるためJavaScriptとCSSの2つのファイルを編集することにしました。
JavaScript
・MAXの値と選ばれた数字によって色を割り当てる
 本ツールでは、数字の入ったdiv要素に、色名を含んだクラス名を追加しています。
 色は8色(白、黒、赤、青、黄、緑、オレンジ、ピンク)です。
CSS
・割り当てられた色に応じたスタイルを設定する

1. MAXの値と選ばれた数字によって色を割り当てる

まずはJavaScriptの処理について説明します
当初は、MAXの値によって5の分岐、選ばれた数字によって8の分岐、合計40の分岐になる想定でした。

・MAXが18の場合
・MAXが17の場合
・MAXが16の場合
・MAXが9以上16未満の場合
・MAXが8以下の場合

筆者のスキルの至らなさにより、MAXの値によって9の分岐、選ばれた数字によって8の分岐、合計72の分岐を手打ちでゴリ押しすることになりました。

・MAXが18の場合
・MAXが16以上の場合
・MAXが15の場合
・MAXが14の場合
・MAXが13の場合
・MAXが12の場合
・MAXが11の場合
・MAXが10の場合
・MAXが9以下の場合

下記はMAXが18の場合です。MAXの値によってスイッチし、さらに選ばれた数字によってスイッチし、枠色を決定(=数字のdiv要素のクラス名を決定)します。

    switch(true){
        case max == 18:
            switch(true){
                case choiceArray.at(-1) >= 16:
                    bracketColor = "bracketColor_pink";
                    break;
                case choiceArray.at(-1) >= 13:
                    bracketColor = "bracketColor_orange";
                    break;
                case choiceArray.at(-1) >= 11:
                    bracketColor = "bracketColor_green";
                    break;
                case choiceArray.at(-1) >= 9:
                    bracketColor = "bracketColor_yellow";
                    break;
                case choiceArray.at(-1) >= 7:
                    bracketColor = "bracketColor_blue";
                    break;
                case choiceArray.at(-1) >= 5:
                    bracketColor = "bracketColor_red";
                    break;
                case choiceArray.at(-1) >= 3:
                    bracketColor = "bracketColor_black";
                    break;
                case choiceArray.at(-1) >= 1:
                    bracketColor = "bracketColor_white";
                    break;
            };
            break;

当初の想定だった分岐が40なら比較的少ないのでゴリ押しでも通用しますが、自分で書いていてダサいと思いました。72分岐を手打ち。アカン。
参考にした色の割り当ての法則性がわかっているので、テクニカルな処理で行数を減らすことも可能だと感じます。可読性と実装の難易度、そして筆者のスキルを考慮した結果です。自分でも納得がいっていない部分なので、より処理効率が高いロジックを思いついたらアップデートしたいです。

2. 割り当てられた色に応じたスタイルを設定する

次はCSSです。
割り当てられたクラス名に応じて、背景色を入れるようCSSを編集します。
各色のカラーコードは、参考となったページを元に決定しています。

また『枠』ごとに色が決められていて(1枠・白/2枠・黒/3枠・赤/4枠・青/5枠・黄/6枠・緑/7枠・橙/8枠・桃)、ジョッキーはその色のヘルメットを被ることになっています。出走取消や競走除外などがあっても“詰める”ことはせず、出馬表が確定した段階での『枠』がそのまま適用されます。

枠連とは? 出走馬を8つの『枠』に分ける馬券=枠連の特徴を紹介 より引用

各色を背景色として設定します。
黒のみ、数字部分を白(#FFFFFF)にするよう指定しています。

/* 枠色の割り当て */
div#output > div.bracketColor_white {
    background-color: #FFFFFF;
}

div#output > div.bracketColor_black {
    background-color: #000000;
    color: #FFFFFF;
}

div#output > div.bracketColor_red {
    background-color: #fe0000;
}

LV.5 順不同モードと昇順モードに切り替えられるようにする

LV.4では選ばれた数字を順不同に表示しています。ランダム感満載なのでこれもいいのですが、数字を小さい順に表示するのも悪くないでしょう。
ツール上の表記は、順不同のものは何だか単々としているので「たんたん」、昇順のものは何だか複々としているので「ふくふく」と名付けます。

1. htmlにモードを切り替えるラジオボタンを追加する

モードを切り替えるための入力方法を考えます。
選択肢はふたつなので、ラジオボタンで問題ないでしょう。
htmlに選択肢のラジオボタンとラベルを追加します。順不同モードを既定の値にしてみましょう。

<label>モード:</label>
<input type="radio" id="mode_tan" name="mode" value="tan" checked />
<label for="tan">たんたん</label>
<input type="radio" id="mode_fuku" name="mode" value="fuku" />
<label for="fuku">ふくふく</label>

2. 昇順モードを追加する

昇順モードを追加する為のロジックを考えます。
順不同モードでは、1つのforループで数字の選択から出力まで完結できました。

・数字を選ぶ
・選んだ数字を出力する
・(選択する数に応じて「>」を追加する)

一方の昇順モードでは、数字の昇順ソートを挟んで2つのforループを回すことになります。

ループA
・数字を選ぶ

・選んだ数字を昇順にソートする

ループB
・(選択する数に応じて「-」を追加する)
・ソートした数字を出力する

まずループAを実装します。順不同モードのforループから枠色の割り当てと数字の出力処理を除外します。

// 数字の数だけランダムに数字を取得する
for(let i = choice; i > 0; i--){
    // 取得した数字を出力用配列に追加
    randomIndex = Math.floor(Math.random() * maxArray.length);
    choiceArray.push(maxArray[randomIndex]);

    // 既に取得した数字を番号配列から削除
    maxArray.splice(randomIndex,1);
}

選んだ数字を昇順にソートする為の関数を宣言します。

const sortChoice = (first,last) =>{
    return first - last;
}

上記の関数を引数に、出力用配列に対してsortメソッドを実行します。

// 選択した数字を昇順にソート
choiceArray.sort(sortChoice);

ループBです。

for(let j = 0; j < choiceArray.length; j++){
    // 枠色の割り当て
    objectIndex = j
    allotColor()

    // 現在の値が1以上の場合、-を追加する
    if(j >= 1){
        arrow = document.createElement('div');
        arrow.classList.add('arrow')
        arrow.innerHTML = "-";
        outputElement.appendChild(arrow);
    }

    // 取得した数字をdiv要素として出力
    choiceElement = [];
    var outputchild = choiceArray[j];

    choiceElement[outputchild] = document.createElement('div');
    choiceElement[outputchild].classList.add(bracketColor);
    choiceElement[outputchild].style.width = "50px";
    choiceElement[outputchild].innerHTML = choiceArray[j];
    
    outputElement.appendChild(choiceElement[outputchild]);
};

ループBでの順不同モードとの相違点を説明します。
枠色の割り当ての時のインデックス指定を変更しています。昇順モードは現在示しているインデックスの要素に枠色を割り当てる動作にしています。
これに伴い、枠色の割り当て関数および順不同モードを修正しています。詳細は[順不同モードとの整合性]で説明します。

// 枠色の割り当て
objectIndex = j
allotColor()

数字の間に表示する記号について。
順不同モードでは「数字の出力後、後ろに数字を出力するなら">"を出力する」、昇順モードでは「数字の出力前、前に数字が出力されているなら"-"を出力する」という動作にしています。
順不同モードと区別する為、記号を「>」から「-」に変更しています。

// 現在の値が1より大きい場合、-を追加する
if(j >= 1){
    arrow = document.createElement('div');
    arrow.classList.add('arrow')
    arrow.innerHTML = "-";
    outputElement.appendChild(arrow);
}

以降の処理は順不同モードとほぼ同じです。

順不同モードとの整合性

順不同モードと共通して使用している関数のひとつに、枠色の割り当て関数があります。
この関数は配列の最後尾の要素を対象に枠色を割り当てる動作をしています。この動作だと昇順モードの動作と噛み合いません。

例えばこの状態で、MAXが15,CHOICEが3を指定して昇順モードで数字を選択するとします。
選ばれた数字が[4,8,13]だとしたら、色はそれぞれ[赤、黄、橙]が割り当てられるのが正常な動作です。
ところが関数で指定しているのは配列の最後尾の要素であるため、関数は最後の要素である13しか見ていません。結果として選ばれた数字に割り当てられるのは13に割り当てられた色であるため、色の割り当てが[橙、橙、橙]となってしまいます。

枠色の割り当て関数はゴリ押しで実装した部分ですが、さすがにここでもゴリ押ししてモード毎に関数を宣言するわけにはいきません。順不同モード昇順モードでも正常に動作するように関数を修正します。
変数objectIndexを用意して、現在処理している配列の位置を格納します。

・順不同モードの場合

// 枠色の割り当て
objectIndex = choiceArray.indexOf(choiceArray.at(-1))
allotColor()

・昇順モードの場合

// 枠色の割り当て
objectIndex = j
allotColor()

関数を修正します。
変数objectIndexの位置になるよう、インデックスの指定を修正します。これで順不同モード、昇順モードともに想定した枠色に割り当てられます。

switch(true){
        case max == 18:
            switch(true){
                case choiceArray[objectIndex] >= 16:
                    bracketColor = "bracketColor_pink";
                    break;
                case choiceArray[objectIndex] >= 13:
                    bracketColor = "bracketColor_orange";
                    break;
                case choiceArray[objectIndex] >= 11:
                    bracketColor = "bracketColor_green";
                    break;
                case choiceArray[objectIndex] >= 9:
                    bracketColor = "bracketColor_yellow";
                    break;
                case choiceArray[objectIndex] >= 7:
                    bracketColor = "bracketColor_blue";
                    break;
                case choiceArray[objectIndex] >= 5:
                    bracketColor = "bracketColor_red";
                    break;
                case choiceArray[objectIndex] >= 3:
                    bracketColor = "bracketColor_black";
                    break;
                case choiceArray[objectIndex] >= 1:
                    bracketColor = "bracketColor_white";
                    break;
            };
            break;

※余談
筆者はMicrosoft製品と連携するPowerShellスクリプトの開発経験があり、ループBの実装については当初はforEachを使用する予定でした。forEachとは、対象となる配列の全ての要素に対して反復処理をするループです。JavaScriptのforで表現するなら「初期化式」「条件式」「加算式」を指定しなくとも反復処理ができる便利なループです。
about_Foreach

javaScriptにもforEachは存在します。ただ、10年間傍らでJavaScriptを見てきただけの人間にはおいそれと実装しても十分な理解は進まないと感じ、forを使用しました。
Array.prototype.forEach()

まさにズバリな記事もあったので、こちらを参考に理解を深めます。
JavaScript forEachを何度も調べがちな方々へ

開発後筆者コメント

2024/07/28
「先人達の知恵に助けられました。ランダムの数字を返す処理のアタリは良かったですが、ページのリソースを読み込む処理をする、という発想にたどり着くまで少し時間がかかりました。慣れない分野ではありましたし作りこみも甘いですが、想定した結果を出力できたのは良かったです。追加の機能実装については、自分自身と相談しながら決めたいと思います。」

2024/08/09
「自分が実装したいと考えていた機能は実装できましたが、コードはあまりスマートではないと感じています。出力範囲のデザインを整えたいところです。」

2024/08/13
「デザインは整ってきましたが、まだ完成とは言えません。ローテーションがタイトなのも原因なのか、今一つ力が出し切れてないです。本人も疲れている様子なので放牧に出そうかと。次の実装は未定です。自分自身と相談します。」

2024/08/19
「終始落ち着きがなく、ロスが多くてもったいないです。いい歳なのでプログラミングを覚えてほしいです。」

2024/08/21
「細かい部分のブラッシュアップはまだまだ必要ですが、中1日でやれるだけのことはやりました。」

想定される質問とその回答

Q.何故公開したのか。
A.自己満足です。

Q.何故選ばれる数の上限が18までなのか。
A.個人的な経験として、18がキリがいいと思いました。6や9でも良いのですが、18が一番馴染みがあります。

Q.何故選ぶ数が3までなのか。
A.個人的な経験として、3がキリがいいと思いました。

参考文献

<input type="number">
Math.random()
[JavaScript]onloadイベントについて

JavaScript forEachを何度も調べがちな方々へ
Array.prototype.sort()
Array.prototype.forEach()

Discussion