年間行事予定をGoogleカレンダーに飛ばすやつ

## Googleスプレッドシートから予定をGoogleカレンダーに飛ばすだけのやつ

### はじめに

Googleカレンダーに手作業で予定を入れるのが面倒になったのでよく見る形式でのシンプルなやつが欲しくなった。
ついでに、「あ！！！！まちがって飛ばしちゃった！」のときも削除もできるようにできればいいなあと思って追加してみた。


---

### ここからテンプレ(スプレッドシート)をコピーしてください。
https://docs.google.com/spreadsheets/d/16Gqj0bti9WkIgIRVm1dpB6cuqUM-ybkitNDIoDD6rlY/copy

- スプレッドシートに、**1年間分の予定**が `B2:M32` に入っている
- `A1` に年度（例：2025）が入っている
- 予定は日付とセットで1セルに1件。(改行してあれば→複数件登録)
- 空欄は無視される＆ありえない日付(2/31等)もエラー処理されます。

#### スプレッドシート画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3648082/5007dfaf-a4fd-49f2-9082-13ef8e6cf536.png)


※B列が4月、M列が3月。つまり、1月〜3月は「次の年」として扱う。

---

### やること

①予定を入力する。※A1(黄色のセル)に年度を入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3648082/ae295f3d-d5fb-41ab-909e-ea0985d842bd.png)
<br>


②Googleカレンダーに終日イベントとして追加(または削除)する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3648082/d2dd0618-bd23-4ac5-b398-fb21d092f5eb.png)
<br>

③イベントチェック※不正な日付（例：2月30日など）はスキップし、確認ダイアログに表示する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3648082/c2489398-fbe6-4585-8233-beec2535d22c.png)
<br>

④カレンダーに追加される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3648082/4fbd28e2-f0f9-4c6f-92e5-8a2c0102c480.png)
<br>

---


## スクリプト全体
####  onOpen：メニューを追加する
```function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu(' カレンダー操作')
    .addItem('予定を追加', 'confirmAddEvents')
    .addItem('予定を削除', 'confirmDeleteEvents')
    .addToUi();
}
```
####  getEventListFromSheet：予定一覧を取得する（不正日付も分けて）
```
function getEventListFromSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const year = sheet.getRange("A1").getValue();
  const values = sheet.getRange(2, 2, 31, 12).getValues(); // B2:M32

  const validEvents = [];
  const invalidDates = [];

  for (let col = 0; col < 12; col++) {
    let month = (col + 4) % 12 || 12;
    let eventYear = (month >= 4) ? year : year + 1;

    for (let row = 0; row < 31; row++) {
      let title = values[row][col];
      if (title !== "") {
        let day = row + 1;
        let date = new Date(eventYear, month - 1, day);

        if (date.getDate() === day && date.getMonth() === month - 1) {
          let dateStr = Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd');
          validEvents.push({ dateStr, title, date });
        } else {
          invalidDates.push(`${eventYear}年${month}月${day}日`);
        }
      }
    }
  }

  return { validEvents, invalidDates };
}
```
解説：
B2:M32 の中身をすべてチェックして、日付として正しいものは validEvents に、そうでないものは invalidDates に分けて返す。
2月30日とかは無視しておく。これがないと事故る。

####  confirmAddEvents：予定を追加する前に確認表示 → 追加実行
```function confirmAddEvents() {
  const { validEvents, invalidDates } = getEventListFromSheet();
  const ui = SpreadsheetApp.getUi();

  if (validEvents.length === 0 && invalidDates.length === 0) {
    ui.alert("追加できる予定がありません。");
    return;
  }

  let list = validEvents.map(e => `${e.dateStr}：${e.title}`).join('\n');
  if (invalidDates.length > 0) {
    list += `\n\n⚠ 以下の日付は存在しないため処理されません：\n${invalidDates.join('\n')}`;
  }

  const result = ui.alert("以下の予定をカレンダーに追加しますか？", list, ui.ButtonSet.OK_CANCEL);

  if (result === ui.Button.OK && validEvents.length > 0) {
    const calendar = CalendarApp.getDefaultCalendar();
    validEvents.forEach(e => calendar.createAllDayEvent(e.title, e.date));
    ui.alert("予定をカレンダーに追加しました。");
  }
}
```
解説：
処理対象の予定を一旦表示して、OK を押されたら追加。
無言で実行すると私は連打したくなるので、ワンクッション。終日イベントとして登録される。

####  confirmDeleteEvents：予定削除の確認 → 実行
```function confirmDeleteEvents() {
  const { validEvents, invalidDates } = getEventListFromSheet();
  const ui = SpreadsheetApp.getUi();

  if (validEvents.length === 0 && invalidDates.length === 0) {
    ui.alert("削除対象の予定がありません。");
    return;
  }

  let list = validEvents.map(e => `${e.dateStr}：${e.title}`).join('\n');
  if (invalidDates.length > 0) {
    list += `\n\n⚠ 以下の日付は存在しないため処理されません：\n${invalidDates.join('\n')}`;
  }

  const result = ui.alert("以下の予定をカレンダーから削除しますか？", list, ui.ButtonSet.OK_CANCEL);

  if (result === ui.Button.OK && validEvents.length > 0) {
    const calendar = CalendarApp.getDefaultCalendar();
    validEvents.forEach(e => {
      const sameDayEvents = calendar.getEventsForDay(e.date);
      sameDayEvents.forEach(ev => {
        if (ev.isAllDayEvent() && ev.getTitle() === e.title) {
          ev.deleteEvent();
        }
      });
    });
    ui.alert("予定を削除しました。");
  }
}
```
解説：
予定を追加 の逆。確認 → 削除。
同じ日付・同じタイトルの終日イベントだけを消す仕様。
事故防止のため、タイトル一致も確認している。



###  後日追記：セル内の改行で複数予定を追加する対応

1セルに複数行で予定を書いているとき、それぞれを別のイベントとして登録したい。
改行できないから苦肉の策


これを **1日2件の予定**としてGoogleカレンダーに登録する。  
もちろん、削除時も同じ条件で一致したものだけ削除するようにする。

---

###  修正対象：予定の抽出関数 `getEventListFromSheet`

```
function getEventListFromSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const year = sheet.getRange("A1").getValue();
  const values = sheet.getRange(2, 2, 31, 12).getValues(); // B2:M32

  const validEvents = [];
  const invalidDates = [];

  for (let col = 0; col < 12; col++) {
    let month = (col + 4) % 12 || 12;
    let eventYear = (month >= 4) ? year : year + 1;

    for (let row = 0; row < 31; row++) {
      let rawText = values[row][col];
      if (rawText !== "") {
        let day = row + 1;
        let date = new Date(eventYear, month - 1, day);

        if (date.getDate() === day && date.getMonth() === month - 1) {
          let dateStr = Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd');
          
          // 改行で分割して1行ずつ予定にする
          let lines = rawText.toString().split(/\r?\n/);
          lines.forEach(title => {
            if (title.trim() !== "") {
              validEvents.push({ dateStr, title: title.trim(), date });
            }
          });

        } else {
          invalidDates.push(`${eventYear}年${month}月${day}日`);
        }
      }
    }
  }

  return { validEvents, invalidDates };
}
```
解説：
.split(/\r?\n/) で改行ごとに分割
.trim() で空白除去（空行対策）
title ごとに validEvents に個別追加
削除処理も .getTitle() === title で完全一致のみ削除

---

##  おわりに

GoogleスプレッドシートとApps Scriptを使って、  
予定をGoogleカレンダーに一括で追加・削除する処理を作りました。

- 予定の元データは月日形式で一覧入力
- 改行で複数予定も対応
- 不正な日付はスキップして警告表示
- メニューから使えるUIつき

備忘録でした。