【0からGASを学ぶ】GASを用いてGoogleフォーム内の項目を自動更新しよう

# はじめに
本シリーズでは、GASの始め方や便利な使い方、ビジネス活用まで幅広く解説します。シリーズをひと通り読んでいただければ、あなたもきっとGASマスターになれるはずです。

# シリーズの対象者
* そもそもGASってなんだか**わからない**方
* GASを学びたいけど**何から始めればいいか**わからない方
* GASはわかり始めたけど、**もっと活用ができないか**と模索している方
* とにかく**Googleが好き！** という方

# 前回記事
https://qiita.com/gas-suke/items/c682d408d2662241f922

# GoogleフォームをGASで更新する
では早速始めていきましょう。【0からGASを学ぶ】シリーズの第18回は「GASを用いてGoogleフォーム内の項目を自動更新しよう」です。前回までは**Gemini Pro APIの無料期間**がいつまで続くかわからないため、**急遽Geminiシリーズを間髪入れずにお伝え**しました。
少し話を戻して、今回はまたGoogleフォームに関する**小ネタ**をお伝えします。第18回ではGoogleフォーム内の**質問項目をGASから更新**するというのをやってみます。また、様々なフォーム項目に関する更新ではなく、ピンポイントにプルダウンの更新を行い、簡易的な**Googleフォームによる予約受付フォームを実現**してみます。まずはこれでイメージを膨らませ、次回Googleフォームで提供する各種項目の追加方法をお伝えすることにします。


## 今回やること
1. Googleフォームのフォーム項目をGASで取得する。
1. 1で取得した値を用いてスプレッドシートを更新する。
1. スプレッドシートの内容を用いてGoogleフォームを更新する。
1. 予約受付結果をメールで送信する。

## 事前準備
### Googleフォームを作成
今回の起動元となるフォームを作成していきましょう。どのようなフォームでもよいのですが予約受付フォームっぽく以下のようなフォームを作成しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/81e1b830-032e-407d-d6ed-e52b189742f4.png" width="600">

| 項目名 | タイプ | 必須 |
|:--|:--|:--:|
| 予約者氏名 | 記述式 | 〇 |
| 予約者メールアドレス | 記述式 | 〇 |
| 予約日 | プルダウン | 〇 |

参考に私が作成したフォームは以下となります。

https://forms.gle/W8ba5XJB3tNQLUj69

### 予約日空き状況を管理するスプレッドシートを作成
Googleスプレッドシートを用いて、予約日空き状況を管理するシートを作成しましょう。どんな様式でも構いませんがこのシートの**カラム順を前提に今回のプログラムは作成**します。なお、私は以下のように作成しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/4149896b-bc45-f24c-b190-d5160ca7f7b3.png" width="600">

:::note info
このシートの予約受付日に記載の2024/1/1から1/8までを、先述の**Googleフォームの予約日に初期セットアップ**しています。
:::

### IDの取得
`予約日空き状況シート`のIDは事前に取得しておきましょう。

### 試しに一度回答
Googleフォームを作成したら、右上の「プレビュー」ボタンを押下して、一度回答してみましょう。のちほど**回答データを取得するためだけ**なので、どのような回答でも構いません。

## プログラム開始
### GASエディタを開く
準備したGoogleフォームから**コンテナバインド型でGASプログラム**を記述していきましょう。Googleフォームからコンテナバインド型のGASを開く場合は、<a href="https://qiita.com/gas-suke/items/cf4a809d54464a43f3ac#gas%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E3%82%92%E9%96%8B%E3%81%8F" target="_blank">こちら</a>を参考にしてください。では、どんどんいきますよ、ついてきてください。

### STEP.1　Googleフォームのフォーム項目をGASで取得する
```javascript:Qiita018
function Qiita018_formSubmit(e) {
  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = FormApp.getActiveForm().getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }
  console.log(itemResponses);
}
```

```:実行結果
21:02:40	お知らせ	実行開始
21:02:40	情報	[ { toString: [Function],
    getFeedback: [Function],
    getItem: [Function],
    setFeedback: [Function],
…
    getResponse: [Function],
    getScore: [Function],
    setScore: [Function] } ]
21:02:41	お知らせ	実行完了
```
これだとどんな値がとれているかはわかりませんが、ばっちりとオブジェクトは取得できていますので準備万端です。

:::note info
今回も`dayjs`ライブラリを使って**日付の操作**を行うため、<a href="https://qiita.com/gas-suke/items/887d45dabb2df3c5767a#step0dayjs%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B" target="_blank">こちらの記事</a>を参考にライブラリを追加しておいてください。
:::

### STEP.2　1で取得した値を用いてスプレッドシートを更新する
次に、回答内容を用いて予約日空き状況シートを更新していきましょう。では、どのように更新していくかを考えてみます。
1. 回答内容の**予約日と一致する予約受付日**を探す
1. 予約受付日の**空き枠が予約済を超えていないか**を確認する
1. 超えていなければ予約可とし、**予約済をインクリメント**する

といった具合になるかと思います。

```diff_javascript:Qiita018
+// Qiita018_予約空き状況
+const SPREAD_ID = '**事前準備で取得しておいたIDを記述**';
+// Qiita018_予約空き状況シートのカラム情報
+const COL = {
+  DAY:1,
+  FREE:2,
+  RESERVE:3
+};
+
/**
 * 予約受付フォームが送信された際のトリガーメソッド
 * @param {EventObject} e - イベントオブジェクト
 */
function Qiita018_formSubmit(e) {
+  const activeForm = FormApp.getActiveForm()
+        , wSheet = SpreadsheetApp.openById(SPREAD_ID).getSheets()[0]
+        , vals = wSheet.getDataRange().getValues();
+
  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = activeForm.getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }
-  console.log(itemResponses);
+
+  vals.shift();
+  // 回答から空き状況を更新する
+  itemResponses.forEach(function(itemResponse){
+    switch (itemResponse.getItem().getTitle()) {
+      case '予約日':
+        vals.forEach(function(val, idx){
+          // 予約日と台帳上の日付を比較して同日を判定
+          if (dayjs.dayjs(val[COL.DAY-1]).isSame(dayjs.dayjs(itemResponse.getResponse()), 'd')) {
+            // 空き枠 > 予約済なら予約成功
+            if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
+              val[COL.RESERVE-1]++;
+              wSheet.getRange(idx+2, COL.RESERVE).setValue(val[COL.RESERVE-1]);
+            } 
+          }
+        });
+        break;
+
+      default:
+        break;
+    }
+  });
}
```
これを実行すると、先ほどお試し送信した回答に応じて**予約日空き状況シートの予約済が更新**されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/8902b0ba-10f8-9ee6-0e3d-028151d595d6.png" width="600">

:::note info
ここでのポイントは
```javascript
vals.shift();
```
とし、`wSheet.getDataRange().getValues();`で取得した値リストの**1行目（ヘッダー）を削除**しています。こうすると、ヘッダーかどうかを**ループ処理の中で判定する必要がなくなる**のでおすすめです。
:::

### STEP.3　スプレッドシートの内容を用いてGoogleフォームを更新する
では今回の本題であるGoogleフォームの更新をしていきましょう。フォームを更新する場合や項目を新規に作成する場合は、**どういった項目を使うのかを定義する**ことがポイントです。
#### 更新をする場合
```javascript
FormApp.getActiveForm().getItems().forEach(function(item) {
  item.asXXXXItem().setYYYYYY();
});
```

https://developers.google.com/apps-script/reference/forms/item?hl=ja

#### 新規作成する場合
```javascript
FormApp.getActiveForm().addXXXXItem().setTitle('AAAAAAAAA')
                                     .setYYYYYY();
```

https://developers.google.com/apps-script/reference/forms/form?hl=ja

:::note info
今はざっくりとしたイメージを持つだけで構いません。冒頭にも記載した通り、**各種項目の新規作成・更新方法**を次回お伝えします。
:::

#### では実装してみましょう

```diff_javascript:Qiita018
// Qiita018_予約空き状況
const SPREAD_ID = '**事前準備で取得しておいたIDを記述**';
// Qiita018_予約空き状況シートのカラム情報
const COL = {
  DAY:1,
  FREE:2,
  RESERVE:3
};

/**
 * 予約受付フォームが送信された際のトリガーメソッド
 * @param {EventObject} e - イベントオブジェクト
 */
function Qiita018_formSubmit(e) {
  const activeForm = FormApp.getActiveForm()
        , wSheet = SpreadsheetApp.openById(SPREAD_ID).getSheets()[0]
        , vals = wSheet.getDataRange().getValues()
+        , dateLst = new Array();

  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = activeForm.getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }

  vals.shift();
  // 回答から空き状況を更新する
  itemResponses.forEach(function(itemResponse){
    switch (itemResponse.getItem().getTitle()) {
      case '予約日':
        vals.forEach(function(val, idx){
          // 予約日と台帳上の日付を比較して同日を判定
          if (dayjs.dayjs(val[COL.DAY-1]).isSame(dayjs.dayjs(itemResponse.getResponse()), 'd')) {
            // 空き枠 > 予約済なら予約成功
            if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
              val[COL.RESERVE-1]++;
              wSheet.getRange(idx+2, COL.RESERVE).setValue(val[COL.RESERVE-1]);
            }
          }
        });
        break;

      default:
        break;
    }
  });

+  // 空き枠がある日付のみでフォームを更新する
+  vals.forEach(function(val){
+    if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
+      dateLst.push(dayjs.dayjs(val[COL.DAY-1]).format('YYYY/MM/DD'));
+    }
+  });
+  if (dateLst.length>0) {
+    activeForm.getItems().forEach(function(item) {
+      if (item.getTitle()=='予約日') {
+        item.asListItem().setChoiceValues(dateLst);
+      }
+    });
+  } else {
+    // 全日程が受付終了となれば予約フォーム自体の受付を終了
+    activeForm.setAcceptingResponses(false);
+  }
}
```
上記のようにすると、
1. まだ**あまりがある日程のみ**がフォームで選択できる
1. 全日程の予約が埋まった場合（dateLstの要素が0の場合）、フォームの**受付を終了**する

といった機能が実現できます。

#### 実行例
##### 予約日空き状況
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/4bd67f64-ba2c-a58c-dd0f-b49bf6c9c7ce.png" width="600">

##### 更新後のフォーム
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/ece0fdbb-df1c-97e8-aa81-8de365664181.png" width="600">


### STEP.4　予約受付結果をメールで送信する
これはもうものすごく簡単です。
```javascript
if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
  val[COL.RESERVE-1]++;
  wSheet.getRange(idx+2, COL.RESERVE).setValue(val[COL.RESERVE-1]);
}
```
この箇所で予約ができるか否かを判断しているため、ここで傷をつけましょう。

#### 結論 こんな感じのプログラムができました
```javascript:Qiita018.gs
// Qiita018_予約空き状況
const SPREAD_ID = '**事前準備で取得しておいたIDを記述**';
// Qiita018_予約空き状況シートのカラム情報
const COL = {
  DAY:1,
  FREE:2,
  RESERVE:3
};

/**
 * 予約受付フォームが送信された際のトリガーメソッド
 * @param {EventObject} e - イベントオブジェクト
 */
function Qiita018_formSubmit(e) {
  const activeForm = FormApp.getActiveForm()
        , wSheet = SpreadsheetApp.openById(SPREAD_ID).getSheets()[0]
        , vals = wSheet.getDataRange().getValues()
        , dateLst = new Array;

  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = activeForm.getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }

  vals.shift();
  // 回答から空き状況を更新する
  let replyAddress, reserveErr;
  itemResponses.forEach(function(itemResponse){
    switch (itemResponse.getItem().getTitle()) {
      case '予約者メールアドレス':
        replyAddress = itemResponse.getResponse();
        break;

      case '予約日':
        vals.forEach(function(val, idx){
          // 予約日と台帳上の日付を比較して同日を判定
          if (dayjs.dayjs(val[COL.DAY-1]).isSame(dayjs.dayjs(itemResponse.getResponse()), 'd')) {
            // 空き枠 > 予約済なら予約成功
            if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
              val[COL.RESERVE-1]++;
              wSheet.getRange(idx+2, COL.RESERVE).setValue(val[COL.RESERVE-1]);
              reserveErr = false;
            } else {
              reserveErr = true;
            }
          }
        });
        break;

      default:
        break;
    }
  });

  // 空き枠がある日付のみでフォームを更新する
  vals.forEach(function(val){
    if (!val[COL.RESERVE-1] || Number(val[COL.FREE-1])>Number(val[COL.RESERVE-1])) {
      dateLst.push(dayjs.dayjs(val[COL.DAY-1]).format('YYYY/MM/DD'));
    }
  });
  if (dateLst.length>0) {
    activeForm.getItems().forEach(function(item) {
      if (item.getTitle()=='予約日') {
        item.asListItem().setChoiceValues(dateLst);
      }
    });
  } else {
    // 全日程が受付終了となれば予約フォーム自体の受付を終了
    activeForm.setAcceptingResponses(false);
  }

  // 予約エラーとなった方には予約できなかった旨をメール送信
  if (reserveErr) {
    sendEmailEx(
      replyAddress,
      '【自動送信】予約不可',
      '予約を受け付けることができませんでした。'
    );
  } else {
    sendEmailEx(
      replyAddress,
      '【自動送信】予約可',
      '予約を受け付けました。'
    );
  }
}

/**
 * メール送信の裏技メソッド
 * @param {String} _recipient - 送信先
 * @param {String} _subject - 件名
 * @param {String} _body - 本文
 * @param {Object} _option - 送信オプション
 */
function sendEmailEx(_recipient, _subject, _body, _option) {
  // 引数の内容でメールを下書き保存する
  const mailDraft = GmailApp.createDraft(_recipient, _subject, _body, _option);
  // 下書き保存したメールIDから下書きを取得し、メール送信を依頼する
  GmailApp.getDraft(mailDraft.getId()).send();
}
```

:::note info
Googleフォームを回答がある度に更新しているため、基本的には**回答を送信すれば予約を受け付けることが可能**です。しかしながら、ちょうど**同じタイミングでフォームをひらいた方が複数いた場合、また両者が同じ日付を選んだ場合にダブルブッキング**になることもあります。そのため、念のため、プログラムの中でも予約の可否を判断しています。
:::

### 
# おわりに
お疲れ様でした。
第18回は「GASを用いてGoogleフォーム内の項目を自動更新しよう」ということで、Googleフォーム内の**項目に設定するリストをスプレッドシートで管理**し、**回答を受け付けるごとにGoogleフォームを更新**するということを実現しました。次回は、**各項目の新規作成・更新方法**をお伝えします。今回と次回予定のテクニックは**Googleフォームを業務活用する中で必須のテクニック**となります。ぜひ、皆さんも実業務での活用に取り組んでみてください。引き続き、GASを楽しんでいきましょう！！
記事を読んで、「良いな」や「今後に期待できる！」と感じて頂けたら**いいね**や**フォロー**、**コメント**いただけると幸いです。それではまた次回をお楽しみに！

## ブログでより詳しく解説しています！
以下画像をクリックしてブログにアクセス！！
<a href="https://gas-4u.com/gas17/" target="_blank"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/54bec39c-bdb9-cfae-da73-bf993cbff6aa.jpeg" width="600"/></a>