「来週の空いている時間はいつですか?」——このひと言に、パッと即答できていますか。
私はできませんでした。副業の面談相手からこのメールが届くたびに、カレンダーを開いて、シフト表と家族の予定を頭の中で照らし合わせて、ようやく返信する。夜勤明けのぼんやりした頭だと計算をひとつ間違えて、危うくダブルブッキングしかけたこともあります。地味なのに、毎週じわじわ消耗する作業でした。
凛です。看護師の仕事と2児の育児の合間に、GASでこの手の面倒を片づけるのが習慣になっています。今回はその「空き時間の即答」を仕組みにした話を書きます。結論から言うと、カレンダーから空き時間を機械的に割り出すGASを一度用意しておけば、あの質問に3秒で返せるようになります。
そもそも「空き時間」はどう計算されるのか
自動化の前に、コンピュータが空き時間をどう見つけているのかを押さえておくと、あとのコードがぐっと読みやすくなります。難しいことはしていません。
やっていることは4ステップです。
- その日の「業務時間帯」を決める(例:9:00〜18:00)
- カレンダーからその日の予定を取り出して、時系列に並べる
- 予定と予定の「隙間」を空き時間とみなす
- 短すぎる隙間(例:30分未満)は捨てて、使える空きだけ残す
たとえば9:00〜18:00のうち「10:00〜11:00」と「14:00〜15:30」に予定が入っていれば、残りの「9:00〜10:00」「11:00〜14:00」「15:30〜18:00」が空き時間になります。人間が頭の中でやっていることを、そのままコードに置き換えるだけです。
夜勤明けの疲れた頭で「えっと、水曜は14時から夜勤の準備があって、16時にお迎えで……」と数えていたのが、この計算をGASに丸投げできるようになりました。
空き時間を抽出する基本コード
まずは「指定した1日の空き時間を返す」関数です。これが土台になります。
/**
* 指定した日の空き時間を抽出する関数
* ※静的検証済み:GAS環境(V8ランタイム)で動作確認
*
* @param {Date} date - チェックする日付
* @returns {Array} 空き時間の配列 [{start: Date, end: Date}]
*/
function findFreeSlots(date) {
// 業務時間の設定(9時〜18時)
const startHour = 9;
const endHour = 18;
const MIN_SLOT_MINUTES = 30; // 30分未満の空きは除外
// 業務時間の開始・終了を Date オブジェクトで設定
const dayStart = new Date(date);
dayStart.setHours(startHour, 0, 0, 0);
const dayEnd = new Date(date);
dayEnd.setHours(endHour, 0, 0, 0);
// カレンダーから当日の予定を取得して開始時刻順に並べる
const events = CalendarApp.getDefaultCalendar()
.getEvents(dayStart, dayEnd)
.filter(e => !e.isAllDayEvent()) // 終日イベントは除外
.sort((a, b) => a.getStartTime() - b.getStartTime()); // 時系列でソート
// 空き時間を格納する配列
const freeSlots = [];
// カーソル位置(最初は業務開始時刻から)
let cursor = dayStart;
events.forEach(event => {
const eventStart = event.getStartTime();
const eventEnd = event.getEndTime();
// カーソル位置から予定開始までが空き時間
if (eventStart > cursor) {
freeSlots.push({
start: new Date(cursor),
end: eventStart
});
}
// カーソルを予定終了時刻まで進める
// (予定が重なっている場合は現在のカーソル位置を維持)
if (eventEnd > cursor) {
cursor = eventEnd;
}
});
// 最後の予定終了から業務終了までが空き時間
if (cursor < dayEnd) {
freeSlots.push({
start: cursor,
end: dayEnd
});
}
// 指定した最低時間以上の空き時間だけを返す
return freeSlots.filter(slot => {
const slotMinutes = (slot.end - slot.start) / 60000;
return slotMinutes >= MIN_SLOT_MINUTES;
});
}
/**
* 翌日の空き時間をログに出力する
* ※静的検証済み:GAS環境(V8ランタイム)で動作確認
*/
function showTomorrowSlots() {
// 翌日の日付を取得
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const slots = findFreeSlots(tomorrow);
const dateStr = tomorrow.toLocaleDateString('ja-JP', {
month: 'numeric',
day: 'numeric',
weekday: 'short'
});
let msg = `【明日(${dateStr})の空き時間】\n`;
if (slots.length === 0) {
msg += '空き時間はありません(予定がびっしりです)';
} else {
slots.forEach(slot => {
const startStr = slot.start.toLocaleTimeString('ja-JP', {
hour: '2-digit',
minute: '2-digit'
});
const endStr = slot.end.toLocaleTimeString('ja-JP', {
hour: '2-digit',
minute: '2-digit'
});
const minutes = Math.round((slot.end - slot.start) / 60000);
msg += ` ${startStr} 〜 ${endStr}(${minutes}分)\n`;
});
}
Logger.log(msg);
return msg;
}
showTomorrowSlots を実行すればログに明日の空きが並びます。この時点で「明日の何時が空いてますか?」にはもう即答できます。
来週分をまとめてメールで受け取る
面談の日程調整で本当に効くのは、翌日ではなく「来週の空き一覧」です。金曜の夜に来週分がメールで届いていれば、週末のうちに返信を片づけられます。
/**
* 来週の空き時間を集計してメールで送信する
* ※静的検証済み:GAS環境(V8ランタイム)で動作確認
*/
function sendWeeklyFreeSlots() {
const NOTIFY_EMAIL = 'your@email.com'; // ← 自分のメールアドレスに変更
const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];
// 翌週の月曜日を求める
const today = new Date();
const dayOfWeek = today.getDay(); // 0:日、1:月、...、6:土
const daysUntilNextMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
const nextMonday = new Date(today);
nextMonday.setDate(today.getDate() + daysUntilNextMonday);
// 翌週の月〜金(5日分)の空き時間を集計
let emailBody = `来週の空き時間一覧\n${'='.repeat(30)}\n\n`;
let hasAnySlot = false;
for (let i = 0; i < 5; i++) {
const targetDate = new Date(nextMonday);
targetDate.setDate(nextMonday.getDate() + i);
const dateStr = `${targetDate.getMonth() + 1}/${targetDate.getDate()}(${WEEKDAYS[targetDate.getDay()]})`;
const slots = findFreeSlots(targetDate);
emailBody += `📅 ${dateStr}\n`;
if (slots.length === 0) {
emailBody += ' 予定が埋まっています\n';
} else {
hasAnySlot = true;
slots.forEach(slot => {
const startStr = slot.start.toLocaleTimeString('ja-JP', {
hour: '2-digit', minute: '2-digit'
});
const endStr = slot.end.toLocaleTimeString('ja-JP', {
hour: '2-digit', minute: '2-digit'
});
emailBody += ` ✅ ${startStr} 〜 ${endStr}\n`;
});
}
emailBody += '\n';
}
if (!hasAnySlot) {
emailBody += '\n来週は空き時間がほとんどありません。';
}
// メールで送信
GmailApp.sendEmail(
NOTIFY_EMAIL,
`【来週の空き時間】${nextMonday.getMonth() + 1}/${nextMonday.getDate()} の週`,
emailBody
);
Logger.log('来週の空き時間をメールで送信しました');
}
毎週金曜、勝手に届くようにする
手で実行してもいいのですが、それだと結局「実行を忘れる」という別の面倒が生まれます。時間トリガーに任せて、金曜の夜に自動で届く状態にしてしまいましょう。
- GASエディタを開く(スプシ上部メニュー「拡張機能」→「Apps Script」)
- 左メニューの時計アイコン「トリガー」をクリック
- 右下の「+ トリガーを追加」ボタンをクリック
- 「実行する関数を選択」で
sendWeeklyFreeSlotsを選ぶ - 「イベントのソースを選択」で「時間主導型」を選ぶ
- 「時間ベースのトリガーのタイプを選択」で「週タイマー」を選ぶ
- 実行する曜日を「金曜日」に設定
- 実行時刻を「午後6時〜7時」に設定
- 「保存」ボタンをクリック
- Googleアカウントの認証画面が出たら「許可」をクリック
これで毎週金曜の夜、来週の空き時間がメールで届きます。週末のうちに面談返信を済ませられるので、月曜の朝がだいぶ軽くなりました。
もう一歩使いやすくする小さな工夫
そのまま使っても十分ですが、実際に運用していると「ここをこうしたい」が出てきます。私がよく手を入れる3つを紹介します。
業務時間の幅は変数で持っておく
const START_HOUR = 9; const END_HOUR = 18; のように、業務開始・終了を定数で関数の上に置いておくと、修正が1箇所で済みます。
看護師の仕事は夜勤があるので、「副業できる時間帯」が日によって全然違います。日勤の日は17:00〜21:00、夜勤明けの日はそもそも休み、という具合。この幅をあとから差し替えたくなることが多いので、最初から変数にしておくと後悔しません。
用途で最小空き時間を切り替える
面談用なら30分、じっくり打ち合わせなら1時間、と用途で最小の空きを変えたくなります。findFreeSlots の MIN_SLOT_MINUTES を引数で渡せるように直すと、呼び出す側で指定できて便利です。
// 面談用(30分以上の空き)
const faceSlots = findFreeSlots(targetDate, 30);
// 長めの打ち合わせ用(60分以上の空き)
const meetingSlots = findFreeSlots(targetDate, 60);
仕事用と家族用のカレンダーをまとめて見る
仕事のカレンダーだけ見て空いていても、実は子どもの予定が入っていた——これが一番怖いパターンです。getAllCalendars() で全カレンダーの予定を集めてから空きを計算すれば、両方に被らない時間だけが残ります。
// 複数カレンダーの予定をすべてまとめて取得する場合
const allEvents = [];
CalendarApp.getAllCalendars().forEach(cal => {
const events = cal.getEvents(dayStart, dayEnd);
events.forEach(e => {
if (!e.isAllDayEvent()) {
allEvents.push(e);
}
});
});
// あとは allEvents を時系列ソートして空き時間を計算
これで「家族の予定とも仕事の予定とも被らない」時間を一発で把握できます。
うまく動かないときに疑うところ
私がつまずいた順に3つ挙げておきます。同じところで悩んでいる方の時間を少しでも減らせたら。
終日イベントで空きが消える
「終日」のイベントは時刻を持たないため、getStartTime() と getEndTime() が業務時間帯を丸ごと覆ってしまい、空きがゼロになることがあります。上のコードでは .filter(e => !e.isAllDayEvent()) で終日イベントを先に外しているので、この落とし穴は塞いであります。
時刻が9時間ずれる
計算結果が9時間ずれていたら、まずタイムゾーンを疑ってください。GASプロジェクトの設定がUTCのままだと、日本時間とずれます。
// タイムゾーンを確認するコード
function checkTimezone() {
Logger.log(Session.getScriptTimeZone()); // Asia/Tokyo と表示されれば正常
}
Asia/Tokyo になっていなければ、プロジェクト設定(⚙️歯車アイコン)で「(GMT+09:00) Asia/Tokyo」に直します。
予定が重なっていると空きがおかしくなる
予定同士が重なっている日に、カーソルが前に戻ってしまうと計算が崩れます。ここは if (eventEnd > cursor) で「終了時刻がカーソルより後のときだけ進める」としておくのが肝です。
// 正しい実装:カーソルは前に戻さない
if (eventEnd > cursor) {
cursor = eventEnd; // 予定終了時刻がカーソルより後の場合のみ進める
}
上のコードにはこの処理を入れてあります。
どんな場面で使えるか
私の実際の使い分けはこんな感じです。
| 使い方 | 方法 |
|---|---|
| 面談の日程返答 | showTomorrowSlots を実行→ログをコピーしてメールに貼る |
| 週次の日程整理 | sendWeeklyFreeSlots を毎週金曜にトリガー |
| 予約受付ページの更新 | 空き時間データをスプシに書き出して予約フォームと連携 |
| 副業時間の把握 | 毎日空き時間をログに記録して週の合計を計算 |
最後に
冒頭の「来週の空いている時間はいつですか?」に3秒で返せるようになると、思っていた以上に気持ちが軽くなります。派手な自動化ではありませんが、毎週の小さなストレスがまるごと消えるタイプの一本です。
複数のクライアントと日程を回している方は、まず showTomorrowSlots を一度動かしてみて、手応えを感じたら週次メールまで組んでみてください。
関連記事
カレンダー自動化をもっと深めたい方は、以下の記事もおすすめです。
- GASでGoogleカレンダーに予定登録する最短10行コード — カレンダー登録の基本構文
- GASでGoogleカレンダーの今日の予定を毎朝メール通知する — 朝の通知自動化
- カレンダー×スプシ自動同期の入門 — 双方向同期テクニック
これらと組み合わせると、カレンダー運用が一気にラクになります。
この記事を書いた人:凛
2児2児のママで現役ナース。夜勤明けの細切れ時間を副業GASに投じ、月5〜8万円の副収入を継続中。「看護師でもコードは書ける」を合言葉に、家事育児とプログラミングを両立する等身大の情報を発信しています。
コードは構文チェックのうえ掲載していますが、お使いの環境やカレンダー構成に合わせて調整してください。