Googleフォームで定員のある選択肢を作る

こんにちは、GMAです。久しぶりの記事になります。

# はじめに

Googleフォームで定員のある選択肢を作りたいときってありますよね。

たとえば、5日間のイベントをやるとして、どの日程に参加したいかフォームで聞くとします。 **各日程の定員が50名として、先着順にしたなら、定員が埋まった選択肢はすぐクローズしたい** わけです。手作業で定員の埋まった選択肢を消していたのなら、そりゃあ何のためのGoogleフォームなんだってなるわけです。

ちなみに、定員に達したらフォームを丸ごとクローズするやり方は調べたら出てきましたが、 **定員に達した選択肢のみを消すやり方** が (記事を書いている途中は) 見つからなかったので、記事にしてみました。

# 作り方

というわけで早速作り方を説明します。手順は以下の通りです。

1. フォームを作る
2. フォームと連携したGoogleスプレッドシートを編集する
3. フォームのスクリプトエディタを開き編集
4. フォームの送信時にスクリプトが動作するようトリガーを設定

原理としては、

- Googleスプレッドシートに **選択肢の一覧と各項目の定員** を予め記載しておく
- 回答が提出されるたび、 **選択肢の一覧とこれまでの提出内容を見比べて、フォームの選択肢を更新** 
- **すべての選択肢が満員ならば、フォーム自体をクローズ** 

というシンプルなものです。

# 注意

具体的なやり方に入る前に一点だけ注意です。このやり方の場合、 **Googleフォームの回答ページでは、定員のある項目の回答を正しく閲覧できません。** なぜならば、定員に達してしまった項目はフォームの選択肢から削除されてしまうからです。

ただし、この方法を使う際にはかならずGoogleスプレッドシートと連携しており、そちらには **完全な回答** が記録されていきますので、実用上問題になることはあまりないと思います。

:::note warn
フォームを実際に利用する際は、 一通りちゃんと機能するかテストをしてから利用することを強くお勧めします。
:::

# フォームを作る

それでは作り方の詳細を見ていきましょう。
まずは、通常通りにフォームを作りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/21f0a538-ca48-4571-8b51-e646319a2860.png)

サンプルなので、シンプルにしました。

定員になったらクローズしたい項目は **プルダウン** にしてください。(他の形式でも実際は可能ですが、今回はプルダウン前提のスクリプトを紹介します。)
なお、日程の候補はこのタイミングでは書く必要はありません。

また、 **定員になったらクローズしたい項目は原則、必須項目にしてください。**
定員のある項目は、大抵重要な項目であることが多いと思いますが……。

# フォームとスプレッドシートを連携し、編集する
フォームの回答ページから、回答記録用のスプレッドシートを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/340dac65-8cf9-61bf-d37d-e80b3f779a78.png)

次に作成したシートを開きます。出てきたシート名を「フォームの回答 1」から「フォームの回答」に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/c05745af-a2dd-a6ef-c5b5-0ecfe05796b3.png)

新たに候補日と定員を記入するためのシートを作成しましょう。シートの名前は「候補」とします。
1行目は以下の画像のように編集しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/b8367b32-358d-b6bb-835a-37601148e7bb.png)

あとは、「候補」シートの候補日と定員を編集します。例えば以下のような感じです。
**定員が0または未記入の場合、定員無制限として扱います。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/731da596-16c3-a859-1727-3e216146f802.png)

# スクリプトエディタを開き編集

フォームの右上にある「その他＞スクリプトエディタ」を選択し、スクリプトを編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/fff81440-0f22-5b8b-5e90-13ac51f1c526.png)

スクリプトの内容は以下のようにします。
基本的にコピペでOKですが、 **基本設定の部分は編集が必要です。**

:::note info
**シート名と項目名は正確に記入してください。**
実際のシート名の末尾に半角スペースが入っていて、ちゃんと動かないケースなど良くあります。
:::

```javascript:formProgram.gs
////
// 【基本設定】
// ここを編集するだけで使用できます
////

//「https://docs.google.com/spreadsheets/d/xxx/edit」のxxxを入れてください
var spreadSheetID = 'xxx';

// 候補日と定員を書くシート名
var sheetName = '候補';

// 回答のシート名
var answerSheetName = 'フォームの回答';

// 定員のある項目の名前
var questionName = '参加したい日程をどうぞ！(各日程定員50名です)';

////
// 【フォームを更新する関数】
////
function updateForm() {
  ////
  // 【スプレッドシートの情報を取得】
  // 候補と定員を取得し、定員に満たない候補のみを取得します
  ////

  // スプレッドシートをIDで取得
  var sheets = SpreadsheetApp.openById(spreadSheetID);

  // 候補と回答のシートを取得
  var sheet = sheets.getSheetByName(sheetName);
  var answerSheet = sheets.getSheetByName(answerSheetName);

  // 候補のシートのA行の2行目から下の値を配列で取得する
  var sheetLastRow = sheet.getLastRow();
  if (sheetLastRow > 1) {
    // 候補と定員を取得
    var candidate = sheet.getRange(2, 1, sheetLastRow - 1, 2).getValues();
  } else {
    return;
  }

  // 回答のシートの2行目から下の値を配列で取得する
  var answerSheetLastRow = answerSheet.getLastRow();
  if (answerSheetLastRow > 1) {
    var questionNames = answerSheet.getRange(1, 1, 1, answerSheet.getLastColumn()).getValues();
    var colCount = questionNames[0].indexOf(questionName);   
    if (colCount === -1) {
      Logger.log("定員のある項目が見つかりません。questionName の値を確認してください");
    }

    // 必要な部分だけ取得 (2次元配列で返ってくるので、flat()で1次元配列に変換しておく)
    var answerData = answerSheet.getRange(2, colCount + 1, answerSheetLastRow - 1).getValues().flat();
  }

  ////
  // 【Googleフォームの選択肢の上書き】
  //// 

  // フォームの取得
  var form = FormApp.getActiveForm();

  // 質問項目をすべて取得
  var items = form.getItems();

  // 選択肢の作成、更新
  items.forEach(function(item) {
    // 質問項目がquestionNameの項目を探す
    if(item.getTitle() === questionName) {
      var listItemQuestion = item.asListItem();
      // 選択肢を入れる配列
      var choices = [];

      // 候補日を一つ一つ見ていく
      // nameAndCapacity[0]が候補日、nameAndCapacity[1]がその定員
      candidate.forEach(function(nameAndCapacity) {        
        if(nameAndCapacity[0] != "") {
          // 定員無制限かどうか。また、回答が一件もない場合もこっち
          if (!answerData || nameAndCapacity[1] === 0 || nameAndCapacity[1] === "") {
            choices.push(listItemQuestion.createChoice(nameAndCapacity[0]));
          } else {
            // 定員がある場合は定員以上になっていないか確認
            var counter = 0;
            // 何人分キャパが埋まっているかカウント
            for(var i = 0; i < answerData.length; i++) {
              if (nameAndCapacity[0] === answerData[i]) {
                counter++;
              }
            }
            // まだキャパがあれば選択肢に追加
            if (counter < nameAndCapacity[1]) {
              choices.push(listItemQuestion.createChoice(nameAndCapacity[0]));
            }
          }
        }
      });

      if (choices.length > 0) {
        // フォームの回答を受け付ける
        form.setAcceptingResponses(true);
        // 選択肢を上書き
        listItemQuestion.setChoices(choices);
      } else {
        // 満員につき、回答受付終了
        form.setAcceptingResponses(false);        
      }
      return;
    }
  });
}
```
プログラムのテストも兼ねて、一度実行してみましょう！
実行するには、①updateFormを選択し、②実行ボタンを押せばOKです。
実行に成功すると、 **フォームが更新され、スプレッドシートに書いた候補日が選択肢に反映されている** はずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/47a20416-5811-4652-db3f-6d6eafce7e6d.png)

# フォームの送信時にスクリプトが動作するようトリガーを設定
フォームを送信したときに、上記のスクリプトが動作するように設定します。
まず「編集＞現在のプロジェクトのトリガー」を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/42ede7e4-a125-dec0-786f-162a23743fa1.png)
この画面で右下のトリガーを追加を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/69130953-1d57-599b-5595-c9fd20f51ce6.png)
トリガーの設定は以下のようにしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320370/b7801442-f9d8-7be3-8b18-50fcdd63a789.png)
これで作業は終了です！

一通り機能するか確認したのち、フォームを配りましょう！

# ちなみに

もし、定員が最後の一人のときに、二人が同時にフォームに回答していたらどうなるでしょうか。

実はこの場合でも余程のことが無い限りちゃんと動きます。正確には、 **定員のある項目が必須項目ではない場合は、無回答として扱われ、必須項目である場合は無効な回答として、再入力することになります。** これは、遅れて出そうとしたほうは既に存在しない選択肢で提出しようとしたことになるためです。

:::note warn
定員をオーバーする可能性はゼロではない (選択肢の削除には数秒かかるため、申し込みが殺到した場合に項目の削除が追い付かない可能性があります) ので、 **最終的に定員越えしていないかご自身で確認されることをお勧めします。**
:::

# 関連ページ
記事書いた後にちゃんと探したら、似たようなことを実現している例がありました。
[Googleフォームで回答数により選択肢を変更する | Yuka Life](https://yukaobu.wordpress.com/2016/10/23/googleform-2/)

また、以下のページではシンプルに定員越えでフォーム自体をクローズしています(本記事でも一部参考にさせていただきました)。
[Googleフォームで回答上限数を設定する方法(中～上級者向け) | cocono design work](https://cocono-design.com/20200117-1/721/)