Googleカレンダーへの登録が面倒くさい
【追記】その後、UIをHTML化したものを作りました。
2022.08.31追記:月末に投稿すると、イベントの開始日が1ヶ月後ろにズレることがあるバグを修正しました。
Googleカレンダー使ってます。仕事のスケジュールもプライベートな予定も全部Googleカレンダーに入れてます。

デフェクトスタンダードなので大抵のアプリやサービスと連携でき、特に乗り換える理由もなく使い続けています。
遡ってみたら2010年から使ってました。
ただ不満なのはスケジュールの登録操作がちょっと面倒くさいことです。
ひとつのスケジュールを登録するためにあちこちクリックしたり入力する必要があります。

それでもPCでの予定入力は割と楽で、日付と時間を「10:00 歯医者」のように入力して「時間を追加」ボタンをクリックすると、時間とタイトルが設定されます。
「時間を追加」ボタンを押さずに普通に保存じても時間とタイトルを自動的に分離してくれます。ここはよくできていると思います。
ただ、コロン以外の区切りでも判別して欲しいなぁと思います。
使う度に面倒だと思うのは、登録するカレンダーをプルダウンメニューで選択することです。

私の場合、カレンダーは「仕事」と「イベント」の2つしかないので、プルダウンで選ぶほどでもないんですよね。それでもPCでの入力はそれほど苦になりません。
問題はスマホアプリでの入力です。
使い方そのものは難しくはないのですが効率が悪いのです。
入力項目がすべて別々のオブジェクトになっていて、タップと入力を何度も繰り返す必要があります。
日付はカレンダー表示をクリック、時間はいちいちダイヤルセレクターから選ばなきゃなりません。テキストで入れた方が速いです。

このUIだと病院などの受付で予定を入力するときにモタモタしてしまうんですよね。さくさくっと入力したいのです。
Siriで音声入力すると楽なのだが、、、
一番簡単なのは、音声入力でカレンダーに登録することです。
まず、iPhoneの設定>カレンダーのアカウント設定でGoogleカレンダーを追加しておきます。
さらにデフォルトカレンダーをGoogleカレンダーに設定します。
また、Googleカレンダーアプリを入れている場合には、両方から通知が来るとうるさいので、通知を許可するのはどちらか一方だけにしておきます。
あとはSiriを起動して「10月20日の15時に歯医者」などと話しかければ、同期しているGoogleカレンダーにも追加されます。
もちろんApple Watchからも音声で登録できます。
Siriは賢いので「明日」「明後日」「来週の月曜日」といった相対的な日付指定もできます。
とても便利なのですが、たまに同期に時間がかかったり、同期されないことがあります。Googleカレンダー側で予定を削除したのにiPhoneのカレンダー側には残っていたりします。
GoogleアシスタントはGoogle Workspaceのカレンダーに非対応!?
Googleアシスタントアプリを使うと、iPhoneでも「OK、グーグル」でお馴染みのGoogleの音声アシストが使えます。
これでGoogleカレンダーに直接登録すれば同期の問題も無くなるだろうということで、さっそくダウンロードしてみました。

Googleアシスタントアプリを起動して予定を追加しようとすると、なんと
「すみません、Google Workspaceカレンダーの予定の作成にはまだ対応していません。」
と言われてしまいました。
無料のGoogleアカウントなら使えるけど、有料のWorkspace契約者はおいてけぼりみたいです。
そういえば以前も、Googleマップの機能が無料アカウントより劣っていて驚いたことがありました。
やっぱりGoogleは基本的に広告屋さんなんでしょうね。
そんなわけでとりあえず声が出せる環境ではSiriに頼むのが一番楽そうです。
ただ、音声入力は固有名詞とかを聞き間違えられると、とたんに修正が面倒になるんですよね。
直す手間を考えると、最初から手入力した方がいいです。
出先で声を出しにくい場面もありますし、やっぱりテキストでさくっと入力したいなぁと思いました。
とりあえず日付と開始時間、イベント名だけ登録したい
私の場合、とりあえず登録したいのは日付と時間とタイトルです。場所とか備考とかの付加情報はあとから編集すればいいです。
例えば「10 20 10 00 歯医者」と1行のテキストを入れると10月20日の10:00に歯医者という予定が入るようにしたい。
登録先のカレンダーは、特に指定しなければデフォルトカレンダーが自動的に選ばれ、指定したら任意のカレンダーに書き込みという感じだと楽です。
なんとなくアイデアが固まって来たので作ってみることにしました。
Calendar APIはちょっと敷居が高かった
最初はGoogleのCalendar APIを使ったWebアプリを作ろうと思っていました。
で、公式ドキュメントやら色んなブログを読んでみたりしたのですが、自分にはどうも認証まわりの設定がよく分かりませんでした。

Googleですから情報は結構たくさんあるのですが、そもそもOAuthとかの認証系とGoogle Cloud Platformの理解が足りないのでちょっと手に負えないなーという感じ。
この辺は将来の課題としていずれ勉強することにして、別の方法を考えました。
GoogleフォームとGoogle Apps Scriptなら簡単にできそう
困った時のGoogle Apps Scriptです。
GASを使えばAPIよりもずっと簡単にGoogleカレンダーの情報にアクセスできることが分かりました。
要するに、すでにログインしているGoogleアカウントが実行するスクリプトですから、面倒な認証はしなくてよいというわけですね。実際、カレンダーIDだけあればGASから読み書きできちゃいます。
当初はGASでWebアプリを作ってdoGetで入力用のHTMLを表示させて、doPostでデータを受け取って、Googleカレンダーに書き込もうと思っていました。
しかしその後、GoogleフォームでもGASが使えることを知りました。

GoogleフォームにGASを添付するとフォーム投稿をトリガーにして投稿データをプログラムで処理できます。
Googleフォームは画面設計の自由度は低いですが、今回は1行のテキストが入力できればいいのでUIをGoogleフォームに任せても大丈夫そうです。
えらく前置きが長くなりましたが、そんなこんなでGoogleフォームからGAS経由でGoogleカレンダーに予定を登録するアプリを作りました。
Googleフォームの投稿画面を作る
まずは投稿フォームを作ります。
Googleドライブで「新規」>「Googleフォーム」>「空白のフォーム」を選びます。

フォームに適当な名前をつけます。
質問の種類を「記述式」に変更、質問を「予定を入力」に設定、「必須」ボタンをオンにします。

問題に説明を追加して区切りとして使える文字などを書いておくとよいかもしれません

カレンダーがひとつしかない人はこれだけでOKです。
私はカレンダーが2つあるのでラジオボタンを追加します。

質問に「カレンダーを選択」、回答に「仕事」「遊び」と設定しました。

これは「必須」をつけずにおきます。
何も選んでいなければスクリプト側でデフォルトカレンダーに書き込むようにすれば、入力の手間がちょっとだけ減ります。フォーム側で質問にデフォルト値が設定できるといいんですけどねー。
「回答」タブを選択し、スプレッドシートのアイコンをクリックします。

「新しいスプレッドシートを作成」を選択

回答記録用のスプレッドシートが作られます。

普段、このシートを見ることはないでしょうが、スクリプトがうまく動かなくてもここにはフォームの機能で自動記録されるので、バックアップやデバッグ用としてとても有用です。
試しにプレビュー機能を使って適当なデータを投稿してみます。

投稿した情報が正しくスプレッドシートに書き込まれていればオッケーです。

これでフォームは完成。HTMLを手組みするより楽ちんです。
あとは投稿時に起動するスクリプトを書くだけです。
Googleフォーム用のスクリプトを書く
スクリプトはフォームの編集画面からスクリプトエディタを起動します。

お馴染みのスクリプトエディタが起動します。

今回は以下のプログラムを入力しました。
calenderToPostIDには自分のカスタムカレンダーのIDを設定する必要があります。
function createEventFromFormPost(e) {
FormApp.getActiveForm();//アクセス権限を与えるためのダミーコマンド
const itemResponses = e.response.getItemResponses();
const eventData = itemResponses[0].getResponse();
const calendarSelected = itemResponses[1] ? itemResponses[1].getResponse() : "primary";
let calendarToPostID = "primary";//デフォルトカレンダー
switch (calendarSelected) {//書き込むカレンダーの設定
case "遊び":
calendarToPostID = "-------------- google calender ID ------------------";
break;
}
let calendar = CalendarApp.getCalendarById(calendarToPostID);
const parsedEventData = parseEventDataFromFormData(eventData);
const title = parsedEventData.title || "項目が空でした";
let startTime = new Date(parsedEventData.time);
if (parsedEventData.isAllDay) {
calendar.createAllDayEvent(title, startTime);
} else {
let endTime = new Date(startTime.getTime());
endTime.setHours(startTime.getHours() + 1);//とりあえず開始時間の1時間後を終了時間にする
calendar.createEvent(title, startTime, endTime);
}
}
function parseEventDataFromFormData(formData) {
const data = formData.split(/[\s\.:]/);
// data[0]:月、data[1]:日、(data[2]:時、data[3]:分)、data[.] or data[4]:タイトル
let isAllDay = false;
let title = "";
let eventTime = new Date();
if (eventTime.getMonth() > (data[0] - 1)) { //今月より前であれば来年にする
eventTime.setFullYear(eventTime.getFullYear() + 1);
} else if (eventTime.getMonth() == (data[0] - 1) && eventTime.getDate() > data[1]) {
eventTime.setFullYear(eventTime.getFullYear() + 1);//今月でも日にちが前なら来年にする
}
eventTime.setDate(1);//いったん日付を1にする。月を設定する時のオーバーフロー防止
eventTime.setMonth(data[0] - 1);//Calenderの月パラメーターは0始まりなので1を引く
eventTime.setDate(data[1]);
if (data.length > 4) {
eventTime.setHours(data[2]);
eventTime.setMinutes(data[3]);
title = data[4];
} else { //時間が省略されていたら終日イベントにする
isAllDay = true;
title = data[2];
}
return { time: eventTime, title: title, isAllDay: isAllDay };
}
GoogleカレンダーのIDを取得する
GASからカレンダーにアクセスするためにはカレンダーIDが必要になります。
ただし、Googleカレンダーに最初からあるカレンダーは"primary"という特殊なIDでアクセスできます。カレンダーを追加していない人や、primaryカレンダーだけでいい人はカレンダーIDを調べる必要はありません。
私は遊び用の「イベント」というカレンダーを作っているので、これのIDを調べます。
Googleカレンダーの画面を表示して、マイカレンダーのリストから「設計と共有」を選びます。

下の方にカレンダーIDがあるのでコピーして、上記のスクリプトのカレンダーIDの部分に貼り付けます。IDは最後のgoogle.com
まで含みます。

calendarToPostID = "-------------- google calender ID ------------------";
私のカレンダーはprymaryとイベントの二つだけです。
たくさんのカレンダーを使っている人はフォームとプログラム(switch文のところ)を修正してください。
Googleフォームの投稿をGASのトリガーに設定する
スクリプトが書けたら、トリガーを設定します。
ウインドウ右側のトリガーボタンをクリックしてトリガー設定・編集画面に切り替えます。

ウインドウ右下の「トリガーを追加」ボタンをクリックします。

図のようにトリガーを設定します。起動する関数は「createEventFromFormPost」、イベントの種類は「フォーム送信時」とします。

保存をクリックするとトリガーが設定されます。
このときフォームデータとスプレッドシートへのアクセス権を要求するダイアログがでたら許可します。

テストとアプリURLの取得
あとはテストしてフォームのアドレスを取得するだけです。
フォーム編集画面に戻り、プレビューモードにして、適当な日付と時間、タイトルを入力してみます。区切り文字にはピリオド、スペース、コロンが使えます。


今日より前の日付にすると来年の予定になるので注意してください。
例:11.25.11.10.テスト
グーグルカレンダー上で、11月25日11時10分に「テスト」という予定が設定されればOKです。また、日付とタイトルだけで終日イベントが記録されるかもテストしておきましょう。

うまく行ったら完成です。ブラウザにブックマークするURLを取得します。
「送信」ボタンをクリックします。

「リンク」タブをクリックします。

表示されたURLにアクセスするとフォームが使えます。ブラウザにブックマークしたり、iPhoneのホーム画面に設定して使います。
GoogleフォームからGASで回答を受け取る方法
最後にGoogle Apps Scriptでのフォーム投稿データの処理、カレンダーデータにアクセスする方法やDateオブジェクトについてまとめておきます。
フォームの回答はトリガーに設定した関数の引数として受け取ります。
function createEventFromFormPost(e) {
const itemResponses = e.response.getItemResponses();
const eventData = itemResponses[0].getResponse();
e.response.getItemResponses();
で回答オブジェクトが取得できます。結果は回答の数に応じた配列になります。
例えば、1問目の回答はitemResponses[0]
、2問目の回答はitemResponses[1]
に入ります。
回答の中身を見るには配列の各要素毎に.getResponse()
メソッドを使って読み出す必要があります。
const eventData = itemResponses[0].getResponse();
がそれに当たります。
フォームの質問を増やす
フォームの質問を増やしたときにはitemResponses[]の配列番号を追加、変更する必要があります。
例えば、2問目に「説明」のテキスト入力欄を追加したとします。
この入力文字列は、以下のように受け取れます。
const eventDescripttion = = itemResponses[1].getResponse();
質問が挿入されると、後ろの番号がずれるので、カレンダーの選択は以下のようにitemResponses[2]
で受け取ります。
const calendarSelected = itemResponses[2] ? itemResponses[2].getResponse() : "primary";
あとはカレンダーへの書き込みスクリプトの中でeventDescripttion
を使います。
説明の追加例は「予定作成のオプション(説明、場所、ゲスト、招待)」を参照してください。
フォームへのアクセス許可を出すためにダミー関数を入れる
最初、スクリプトが動かなくて困りました。
調べてみると、スクリプト内にフォーム関連の関数がないと、フォームデータへのアクセス許可を求めるダイアログが表示されないらしいです。
FormApp.getActiveForm();
という一行を入れることで実行時にアクセス権の設定ダイアログが出ます。あらかじめGASエディタのデバッグ機能で関数を直接実行しておくと確実です。
GASでGoogleカレンダーにアクセスする方法
カレンダーオブジェクトを取得する
Googleカレンダーにアクセスするには、カレンダーIDを引数にしてカレンダーオブジェクトを取得します。
const calendar = CalendarApp.getCalendarById(calendar_ID);
今回はletを使いましたがconstでもOKです。
予定を追加する
カレンダーオブジェクトのメソッドで予定を追加します。
予定の追加メソッドは終日イベント.createAllDayEvent
、と時間指定イベント.createEvent
で異なります。
時間はDateオブジェクトで渡します。
//startTime、endTimeはDateオブジェクト、titleはstring
//終日イベント
calendar.createAllDayEvent(title, startTime);
//時間指定イベント
calendar.createEvent(title, startTime, endTime);
予定作成のオプション(説明、場所、ゲスト、招待)
予定の追加メソッド.createAllDayEvent
、.createEvent
は、オプションのオブジェクトを渡すことで、説明、場所、ゲスト、招待の有無を設定できます。
//終日イベント
calendar.createAllDayEvent(title, startTime,{description:文字列,location:文字列,guests:カンマ区切りのメールアドレス,sendInvites:Boolean});
//時間指定イベント
calendar.createEvent(title, startTime, endTime,{description:文字列,location:文字列,guests:カンマ区切りのメールアドレス,sendInvites:Boolean});
オプションは設定したいものだけを渡して、いらないものは省略できます。
例えば、終日イベントに説明を追加したい場合には以下のようになります。
時間指定イベントの場合も同様に最後にオプションのオブジェクト追加します。
//eventDescriptionに説明テキストが入っている場合
calendar.createAllDayEvent(title, startTime,{description:eventDescription});
Dateオブジェクトには関しては別記事で
日付の処理にはDateオブジェクトを使っています。これに関しては別記事でまとめました。
doPostでWebアプリ化しても良さそう
機能的には十分に事足りて満足していますが、Googleフォームの画面がちょっとイマイチかなぁと思います。文字とか入力フィールドを大きくできるとよいのですが。
プログラム的にはdoPost(e)とかでやっても大して変わらないので、Webアプリ化して好きなデザインの投稿画面を作っても良いかなと思います。
【追記】作ってみたら、たいへん使いやすく鳴りました。