オンライン・シャッフルランチを支える技術

# はじめに
20卒新卒エンジニアです．コロナの影響で入社式も延期になり，4ヶ月たった今ですら，出社できない状況です．
ついには内定式以来，**リアルで同期全員と会ったことがありません**．．．

そこで，**強制的にコミュニケーション**を取る場を作ろうと，今回**シャッフルランチ（リモート）**を企画しました！
~~同期のカレンダーに勝手にランチイベントを突っ込む極悪非道の企画です~~

この記事では，オンラインシャッフルランチの企画を支える技術として**GAS**を使って，自動的に**Google Meet**を有効にした状態で**Googleカレンダー**に**ランチイベントを登録**する方法を紹介します．


# やりたいこと
### 参加メンバーをシャッフルして３人グループを作る
※ オンラインで話すなら，これぐらいが程よかったので1グループ3人としました．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/5c141584-24b4-0b7a-fb6e-7e218996d41e.png" alt="グループ化の様子" width=300>

### グループごとにGoogleカレンダーにランチイベントを登録
共有カレンダーを使ってグループごとにランチを招待します
![events.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/c70b11cb-e10b-c7d8-fd58-31ac4eab62af.png)

### Googleカレンダーから直接Google Meetにアクセスできるようにする
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/fbf324bb-1f62-701b-6b09-2b70f5487573.png" alt="イベント内容の様子" width=300>

# 考えるべきこと
### 余る人がでた場合
3人で1グループ作るとすると，余りが1人または2人でる可能性があります
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/dd33db60-da82-fe34-b5fd-c212ce6d4f75.png" alt="グループ化の様子" width=300>

このような場合，以下のようにグループを再編することでボッチを回避しました．

- １人余る場合は，４人グループを１つ作成
- ２人余る場合は，４人グループを２つ作成

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/d21e7028-2922-bb30-5446-1a32222d6888.png" alt="グループ再振り分けの様子" width=300>

### メンバー情報の取得方法 
**カレンダー登録**するためには，メンバーの**メールアドレス**が必要です．

新卒同期が参加する共有カレンダーを作っているので，そこからメンバー情報（メールアドレス）が取得できないか検討しましたが，直接は無理そうでした．

なので，今回は**スプレッドシート**に**メンバーリストを記載**して，そこから取得するようにしました．
詳細は後述します．

# 準備
コーディングを行う前に，いくつか用意することがあります．
このセクションでは，準備事項について詳細に説明します．

## カレンダーID
GASからカレンダーを操作するために，**カレンダーID**が必要です．

取得方法は，**Google Calendar**から**歯車マーク** -> **設定** -> **参加している共有カレンダー**を選択すると，ページ下に赤枠で囲った**カレンダーID**が表示されます．これを使うのでコピーしておきます．

![calendarid.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/195a744c-8c88-386c-2047-308e5f40af36.png)

## スプレッドシート
### スプレッドシートの作成
**Google Drive**の任意の場所に**スプレッドシート**を作成します．

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/03dad4c6-642b-46c8-4885-aa4a69a5227d.png" alt="スプレッドシートの様子" width=350>

### メールアドレスとカレンダーIDを記載
シート名を「**test**」として，

- 1列目に参加するユーザの**メールアドレス**
- 2列目には，先程コピーした**カレンダーID**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/3e21078f-6a2c-b5dc-13bf-300647ae575b.png" alt="スプレッドシートの様子" width=350>

## Calendar API
スプレッドシートのタブから**ツール** -> **スクリプトエディタ**を選択すると，GASエディタが表示されます．
ここで，**Calendar API**を利用するために，タブから**リソース** -> **Googleの拡張サービス**を選択し，**Calendar API**をONにします．

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/a4bda17d-c220-4832-f566-c8b046d64a4e.png" alt="GASの様子" width=350>

以上で，準備が整いました．

# コーディング
最終的なコードはGitHubに上げています．
<a href="https://github.com/sattosan/shuffle-lunch"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/sattosan/shuffle-lunch.png" width="460px"></a>

このセクションでは，作成したコードについてそれぞれ詳細に説明します．

## スプレッドシートの操作
### メールアドレス一覧の取得
スプレッドシートから情報を取得するために，インスタンスを作成しています．
メールアドレスは，**getRange**で2行1列を始点，**getLastRow**で終点を指定して**getValues**で配列として**すべてのメールアドレス**を取得します．

```js:シートの操作
// シートオブジェクトを作成
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 操作するシートのオブジェクトを取得
const sheet = ss.getSheetByName('test');
// ユーザのEmailアドレスをシートから取得
const emails = sheet.getRange(2, 1, sheet.getLastRow() - 1).getValues();
```

### 配列のフォーマット
取得したメールアドレス一覧が格納された配列の中身は以下のようになっています．

```js:初期配列の様子
[['xxx@example.com'], ['xxx@example.com'], ['xxx@example.com']]
```
後述するカレンダー登録でユーザを指定するためには，以下のような形式にフォーマットする必要があります．

```js:フォーマット後の配列
[{'email': 'xxx@example.com'}, {'email': 'xxx@example.com'}, ... , {'email': 'xxx@example.com'}]
```
そのためのコードは以下のようになります．

```js:配列のフォーマット
// Calendar API用にフォーマット
const formatedEmails = shuffle(emails).map(email => ({'email': email[0]}));
```

### カレンダーIDの取得
スプレッドシートの2行2列目にあるカレンダーIDを取得します．

```js:カレンダーIDの取得
// 共有カレンダーのIDをシートから取得
const calendarId = sheet.getRange(2, 2).getValue();
```

## シャッフル
配列の要素をランダムに並び替えるコードは以下のようになります．

```js:配列をシャッフル
// 配列の要素をシャッフル
function shuffle(array){
  var result = [];
  for(i = array.length; i > 0; i--){
    var index = Math.floor(Math.random() * i);
    var val = array.splice(index, 1)[0];
    result.push(val);
  }

  return result;
}
```

## グループの作成
基本的には，３人で１グループを作成します．
グループは以下のように配列にネストした形で表現しています．

```js:グループ化を行った配列の様子
[
  [
   {'email': 'xxx@example.com'},
   {'email': 'xxx@example.com'},
   {'email': 'xxx@example.com'}
  ],
  [
   {'email': 'xxx@example.com'},
   {'email': 'xxx@example.com'},
   {'email': 'xxx@example.com'}
  ]
]
```

余りがでた場合は前述したとおり，**４人グループ**を作成します．
**Switch文**ではその処理をおこなっています．

そもそも人数が少ない場合（５人以下）は，グループ化せずそのまま返すようにします．

```js:グループの作成
// グループ作成
function createLunchGroups(array) {
  const arrayLen = array.length;
  // 作成できるグループ数
  const groupNum = Math.floor(arrayLen / 3);
  var result = [];

  // グループ数が2つ未満だった場合、グループ分けを行わない
  if (groupNum < 2) {
    result.push(array)
    return result;
  }
  // 3人一組でグループ分けを行う
  switch (arrayLen % 3) {
    // 1人余る場合: 4人グループを1つ作る
    case 1:
      // 4人グループを1つ作成
      result.push(array.slice(0, 4));
      // 3人グループを作成
      for (i = 2; i <= groupNum; i++) {
        result.push(array.slice(i*3-2, i*3+1));
      }
      break;
    // 2人余る場合
    case 2:
      // 4人グループを2つ作成
      for (i = 0; i < 2; i++) {
        result.push(array.slice(i*4, i*4+4))
      }
      // 3人グループ作成
      for (i = 3; i <= groupNum; i++) {
        result.push(array.slice(i*3-1, i*3+2));
      }
      break;
    // 3人ちょうどで作れる場合
    default:
      for (i = 1; i <= groupNum; i++) {
        result.push(array.slice(i*3-3, i*3))
      }
      break;
  }

  return result;
}
```
## Googleカレンダーへの登録
カレンダーにイベントを登録をするには「**Calendar.Events.insert**」を使います．

引数には，イベントの詳細情報をパラメータとして渡すことで細かい制御が可能です．

今回は，

- タイトルおよびイベントの説明
- 場所
- 開始日時および終了日時
- Google Meetの設定
- attendsで招待するユーザを指定

をパラメータとして渡しています．

特に**Google Meet**を有効化するには，「**conferenceDataVersion**」の値として1を指定し，「**conferenceData**」をパラメータで渡す必要があります．

```js:コード
// イベント登録
function createEvent(calendarId, users) {
  // リクエストIDをランダムに生成
  const requestId = Utilities.formatString("shuffle#%d", Math.random()*100);
  // イベントの詳細設定
  const detail = {
    summary: 'シャッフルランチ',
    location: 'リモート',
    description: '同期との交流を目的としたランチ会です！',
    start: {
      dateTime: new Date('2020/8/3 12:00:00').toISOString()
    },
    end: {
      dateTime: new Date('2020/8/3 12:30:00').toISOString()
    },
    conferenceData: {
      createRequest: {
        conferenceSolutionKey: {
          type: "hangoutsMeet"
        },
        requestId: requestId
      }
    },
    attendees: users
  };
  // イベントの登録
  const event = Calendar.Events.insert(detail, calendarId, { conferenceDataVersion: 1 });
  Logger.log('Event ID: ' + JSON.stringify(event));
}
```

# 実行
メイン関数として**mainProcess**を定義したので，これを実行します．

```js:メイン関数
function mainProcess() {
  // シートオブジェクトを作成
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  // 操作するシートのオブジェクトを取得
  const sheet = ss.getSheetByName('test');
  // ユーザのEmailアドレスをシートから取得
  const emails = sheet.getRange(2, 1, sheet.getLastRow() - 1).getValues();
  // Calendar API用にフォーマット
  const formatedEmails = shuffle(emails).map(email => ({'email': email[0]}));
  // グループの作成
  const lunchGroups = createLunchGroups(formatedEmails);
  // カレンダーIDをシートから取得
  const calendarId = sheet.getRange(2, 2).getValue();
  // グループごとにイベント登録
  lunchGroups.map(group => createEvent(calendarId, group));
}
```


スクリプトは「Ctrl + R」で実行できます．

実行すると，反映されるまで多少時間かかりますが，３０秒ほどでカレンダーにイベント登録がされていると思います．
![events.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130670/049ff24d-d196-5402-57db-2e7086da7d44.png)

# おわりに
リモートシャッフルランチをいい感じに登録するスクリプトを紹介しました．

なかなかリアルではコミュニケーションが取りづらい状況なので，少しでもオンライン・ランチのハードルが下がり，誰でも気軽に企画できるようになればと思います．