📓

Notionで管理していたやったことリストをGoogleカレンダーで表示できるようにした話[無課金ユーザー向け]

2023/11/17に公開

抱えていた問題

Notionのデータベースを日々のログ管理に使用する場合、一日にどれくらいやったかが一目で分かりづらく、達成感が得られづらいという問題がありました。

図1はNotionのデータベースのリストビューで、図2はNotionのデータベースのカレンダビューです。どちらも一日にやったことの個数は分かりやすいです。しかし、どれくらいの時間やったかが分かりづらいです。

例えば、Googleカレンダーのように時刻まで表示できるカレンダーであれば、やったことを登録していくことで視覚的に一日にどれくらいの時間やったのかが分かりやすくなります。

しかし、リストに記入後、Googleカレンダーにも手作業で登録するのは面倒です。そこで、この入力の手間を減らしたいと考えました。
図1. Notionのやったことリストのテーブルビュー
図1. Notionのやったことリストのテーブルビュー
図2. Notionのやったことリストのカレンダービュー
図2. Notionのやったことリストのカレンダービュー

https://notion-automations.com/ja

制作物

上記の問題に対して、私は、Notionのデータベースをcsvにエクスポートして、それをGoogle Apps Scriptを利用することでGoogleカレンダーに反映させることにしました。制作したものを利用する手順を以下に示します。
利用手順

  1. Notionのページをエクスポートしダウンロード

図3に示すように、Notionのデータベースのページで、右上のメニューボタンを押して、メニューからエクスポートを選択します。そして、表示されたウィンドウのエクスポートボタンを押してzipファイルをダウンロードします。
図3. Notionのページをエクスポートする方法
図3. Notionのページをエクスポートする方法
2. zipファイルを展開

zipファイル展開後のフォルダ構成は以下のようになります。このフォルダの〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇_all.csvファイルを用います。

C:.
│  △△△△△△△△△△△△△△△△△△△.md
│
└─△△△△△△△△△△△△△△△△△△△
        〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇.csv
        〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇_all.csv
  1. csvファイルをGoogleのスプレッドシートにインポート

図4のように、今回、コードの実装を行ったスプレッドシート内で、ファイルタブからインポートをクリックします。

インポートの際の設定は、図5のように、インポート場所を「現在のシートを置換する」としてインポートします。
図4. ファイルタブのインポートボタン
図4. ファイルタブのインポートボタン
図5. インポート設定
図5. インポート設定
4. カレンダーへの登録

スプレッドシート内の「カレンダーに登録」ボタンをクリックして、カレンダーにデータを登録します。このボタンは今回、作成したものの一部です。

登録後のカレンダーの様子を図6に示します。一日にどれくらいの時間やったのかが視覚的に分かるようになりました。
図5. カレンダーに登録
図5. カレンダーに登録
図6. カレンダーの様子
図6. カレンダーの様子

実装方法の説明

実装方法の説明ではまずはじめに大まかな実装の流れを示し、次にコードの細かい説明を示します。

実装の流れ

やったことを追加したいカレンダーのIDをメモ
まずはじめにGoogleカレンダーを開きます。図7のように左側のマイカレンダーからやったことを追加したいカレンダーにカーソルを合わせます。そして、オーバーフローメニューボタンをクリックし、設定と共有をクリックしてカレンダーの設定画面に移動します。

ここで、図8のような「カレンダーの統合」から赤線で囲んだ1番のカレンダーIDを取得します。これをメモしておきます。
図7. Googleカレンダーの設定と共有ボタン
図7. Googleカレンダーの設定と共有ボタン
図8. カレンダーIDの取得
図8. カレンダーIDの取得
スプレッドシート作成
まずはじめにスプレッドシートを作成します。スプレッドシート名はなんでもいいです。

図9のように挿入タブの図形描画ボタンをクリックしてカレンダーに登録ボタンを作成します。後でこのボタンにスクリプトを割り当てます。

図10にカレンダーに登録ボタン追加後のスプレッドシートの様子を示します。赤線で囲んだ2番のスプレッドシートIDと3番のシート名をメモしておいてください。
図9. スプレッドシート作成
図9. スプレッドシート作成
図10. カレンダーに登録ボタン追加後のスプレッドシートの様子
図10. カレンダーに登録ボタン追加後のスプレッドシートの様子
コードの追加
図11に示すように、拡張機能タブのApps Scriptボタンをクリックします。そうするとApps Scriptの編集画面が表示されるのでそこにコードを入力していきます。
図11. コード追加の方法
図11. コード追加の方法
追加するコードを以下に示します。ここで、図8、図10の赤線で囲んだ部分を1番のカレンダーID、2番のスプレッドシートID、3番のシート名に入力していきます。

const CALENDAR_ID = '1. あなたのカレンダーID';
const SPREADSHEET_ID = '2. あなたのスプレッドシートID';
const SHEET_NAME = '3. あなたのシート名';

let myCalendar = CalendarApp.getCalendarById(CALENDAR_ID);
let spreadSheet = SpreadsheetApp.openById(SPREADSHEET_ID);
let sheet = spreadSheet.getSheetByName(SHEET_NAME);

class DoneListDatabase {
  constructor(name, date, tag, detail, startTime, durationTime){
    this.name = name;
    this.date = date;
    this.tag = tag;
    this.detail = detail;
    this.startTime = startTime;
    this.durationTime = durationTime;
  }

  toCalendarItem(){
    let startDate = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), this.startTime.getHours(), this.startTime.getMinutes(), this.startTime.getSeconds());
    let endDate = new Date(startDate).addMinutes(this.durationTime);
    return new CalenderItem(this.name, startDate, endDate, this.detail);
  }
}

class CalenderItem {
  constructor(name, startDate, endDate, description){
    this.name = name;
    this.startDate = startDate;
    this.endDate = endDate;
    this.description = description;
  }

  equal(calendarItem){
    if(!(calendarItem instanceof CalenderItem)){
      return false;
    }

    if(this.name !== calendarItem.name){
      return false;
    }

    if(this.startDate.getTime() !== calendarItem.startDate.getTime()){
      return false;
    }

    if(this.endDate.getTime() !== calendarItem.endDate.getTime()){
      return false;
    }

    if(this.description !== calendarItem.description){
      return false;
    }

    return true;
  }
}

Date.prototype.addMinutes = function(m) {
  this.setTime(this.getTime() + (m*60*1000));
  return this;
}

function registerDataOnCalendar() {
  
  if(sheet.getLastRow() == 0){
    Browser.msgBox('データが入力されていません', Browser.Buttons.OK);
    return;
  }
  let doneList = sheet.getRange(2, 1, sheet.getLastRow(), sheet.getLastColumn()).getValues();
  let newCalendarItems = new Array();
  for(let i = 0; i < doneList.length; i++){
    let doneListDBRow = new DoneListDatabase(doneList[i][0], doneList[i][1], doneList[i][2], doneList[i][3], doneList[i][4], doneList[i][5]);
    if(doneList[i][0] == '' || doneList[i][1] == '' || doneList[i][3] == '' || doneList[i][4] == '' || doneList[i][5] == ''){
      continue;
    }

    let calendarItem = doneListDBRow.toCalendarItem();
    newCalendarItems.push(calendarItem);
  }
  
  let uniqueEvents = getUniqueEvents(newCalendarItems);
  for(let i = 0; i < uniqueEvents.length; i++){
    let event = myCalendar.createEvent(uniqueEvents[i].name, uniqueEvents[i].startDate, uniqueEvents[i].endDate);
    event.setDescription(uniqueEvents[i].description);
  }

  sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn()).clear();
}

function deleteAllEvents(){

  let events = myCalendar.getEvents(new Date(2000, 1, 1, 0, 0, 0), new Date());
  for(let i = 0; i < events.length; i++){
    events[i].deleteEvent();
  }
}

function getUniqueEvents(newCalendarItems){
  
  let oldCalendarItems = new Array();
  let events = myCalendar.getEvents(new Date(2000, 1, 1, 0, 0, 0), new Date());
  for(let i = 0; i < events.length; i++){
    oldCalendarItems.push(new CalenderItem(events[i].getTitle(), events[i].getStartTime(), events[i].getEndTime(), events[i].getDescription()));
  }
  let uniqueEvents = new Array();
  
  for(let i = 0; i < newCalendarItems.length; i++){
    if(oldCalendarItems.find((elm) => newCalendarItems[i].equal(elm)) == null){
      uniqueEvents.push(newCalendarItems[i]);
    }
  }

  return uniqueEvents;
}

スプレッドシートのボタンにスクリプトを登録
スプレッドシートに戻ったら、スプレッドシート上のボタンを右クリックします。ボタンの右上のメニューボタンをクリックしてください。図12のように表示されたメニューから「スクリプトを割り当て」をクリックします。

ポップアップウィンドウで「registerDataOnCalendar」と入力して、確定します。

カレンダーに登録ボタンをクリックすると、最初は「認証が必要です」と表示されますので、認証を完了してください。認証が完了すると、正しくスプレッドシートの情報が入力されている場合は、ボタンをクリックすると「データが入力されていません」と表示されます。

これで制作物の説明で示した利用手順に従ってNotionのデータベースをGoogleカレンダーに登録できるようになりました。
図12. スクリプトをボタンに登録
図12. スクリプトをボタンに登録

コードの細かい説明

これ以降はコードについての説明です。自分専用にコードをカスタマイズしたい場合は参考にしてください。
下準備
まずは、データを登録するカレンダーとデータを入力するシートの取得を行います。

const CALENDAR_ID = '1. あなたのカレンダーID';
const SPREADSHEET_ID = '2. あなたのスプレッドシートID';
const SHEET_NAME = '3. あなたのシート名';

let myCalendar = CalendarApp.getCalendarById(CALENDAR_ID);
let spreadSheet = SpreadsheetApp.openById(SPREADSHEET_ID);
let sheet = spreadSheet.getSheetByName(SHEET_NAME);

次に、DoneListDatabaseクラスとCalenderItemクラスを定義します。

DoneListDatabaseクラスは、スプレッドシートの1行のデータに対応します。CalenderItemクラスは、カレンダーに登録するデータを表現します。

DoneListDatabaseクラスには、CalenderItemクラスに変換するメソッドが実装されています。このメソッドは、スプレッドシートから取得したデータをカレンダーに登録する際に使用します。

CalenderItemクラスには、CalenderItemクラスのオブジェクト同士の等価性を判断するメソッドが実装されています。このメソッドは、カレンダーにすでに登録されているデータを識別するために使用します。

class DoneListDatabase {
  constructor(name, date, tag, detail, startTime, durationTime){
    this.name = name;
    this.date = date;
    this.tag = tag;
    this.detail = detail;
    this.startTime = startTime;
    this.durationTime = durationTime;
  }

  toCalendarItem(){
    let startDate = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), this.startTime.getHours(), this.startTime.getMinutes(), this.startTime.getSeconds());
    let endDate = new Date(startDate).addMinutes(this.durationTime);
    return new CalenderItem(this.name, startDate, endDate, this.detail);
  }
}

class CalenderItem {
  constructor(name, startDate, endDate, description){
    this.name = name;
    this.startDate = startDate;
    this.endDate = endDate;
    this.description = description;
  }

  equal(calendarItem){
    if(!(calendarItem instanceof CalenderItem)){
      return false;
    }

    if(this.name !== calendarItem.name){
      return false;
    }

    if(this.startDate.getTime() !== calendarItem.startDate.getTime()){
      return false;
    }

    if(this.endDate.getTime() !== calendarItem.endDate.getTime()){
      return false;
    }

    if(this.description !== calendarItem.description){
      return false;
    }

    return true;
  }
}

最後に、Date型に指定した分数だけ時間を進めるメソッドを実装します。

このメソッドは、DoneListDatabaseクラスからCalenderItemクラスに変換する際に使用します。これで、下準備は完了です。

Date.prototype.addMinutes = function(m) {
  this.setTime(this.getTime() + (m*60*1000));
  return this;
}

処理のメイン部分の実装
シートデータを一括で配列として取得し、その配列データから一行ずつDoneListDatabaseオブジェクトを生成します。生成したオブジェクトをCalenderItemオブジェクトに変換し、変換後のオブジェクトを配列に格納します。

function registerDataOnCalendar() {
  ......
  let doneList = sheet.getRange(2, 1, sheet.getLastRow(), sheet.getLastColumn()).getValues();
  let newCalendarItems = new Array();
  for(let i = 0; i < doneList.length; i++){
    let doneListDBRow = new DoneListDatabase(doneList[i][0], doneList[i][1], doneList[i][2], doneList[i][3], doneList[i][4], doneList[i][5]);
    if(doneList[i][0] == '' || doneList[i][1] == '' || doneList[i][3] == '' || doneList[i][4] == '' || doneList[i][5] == ''){
      continue;
    }

    let calendarItem = doneListDBRow.toCalendarItem();
    newCalendarItems.push(calendarItem);
  }
  ......
}

次に、カレンダーに登録されていないオブジェクトをgetUniqueEvents関数を利用して取得します。

この関数では、まずカレンダーからイベントを取得し、CalenderItemオブジェクトの配列に格納します。次に、等価メソッドを利用して、新しい配列から既にカレンダーに存在しているオブジェクトを取り除きます。最後に、残った配列を返します。

function registerDataOnCalendar() {
  ......
  let uniqueEvents = getUniqueEvents(newCalendarItems);
  ......
}

function getUniqueEvents(newCalendarItems){
  
  let oldCalendarItems = new Array();
  let events = myCalendar.getEvents(new Date(2000, 1, 1, 0, 0, 0), new Date());
  for(let i = 0; i < events.length; i++){
    oldCalendarItems.push(new CalenderItem(events[i].getTitle(), events[i].getStartTime(), events[i].getEndTime(), events[i].getDescription()));
  }
  let uniqueEvents = new Array();
  
  for(let i = 0; i < newCalendarItems.length; i++){
    if(oldCalendarItems.find((elm) => newCalendarItems[i].equal(elm)) == null){
      uniqueEvents.push(newCalendarItems[i]);
    }
  }

  return uniqueEvents;
}

最後に、重複を取り除いたCalenderItemオブジェクトの配列をカレンダーに登録していきます。
シートのデータは一連の処理が終わった後、消去しています。

function registerDataOnCalendar() {
  ......
  for(let i = 0; i < uniqueEvents.length; i++){
    let event = myCalendar.createEvent(uniqueEvents[i].name, uniqueEvents[i].startDate, uniqueEvents[i].endDate);
    event.setDescription(uniqueEvents[i].description);
  }

  sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn()).clear();
}

カレンダーのイベント削除用
メインの処理とは関係ありませんが、カレンダーに追加したイベントをすべてリセットしたい場合に使用するコードも含まれています。

function deleteAllEvents(){

  let events = myCalendar.getEvents(new Date(2000, 1, 1, 0, 0, 0), new Date());
  for(let i = 0; i < events.length; i++){
    events[i].deleteEvent();
  }
}

最後に

以上、Notionで管理している日時付きデータをGoogleカレンダーに自動登録する方法をご紹介しました。Notionの有料プランで提供されている連携機能には及びませんが、無料プランでもそれなりに楽に自動登録できるようになります。Notionの無料プラン内で使用したい方は、ぜひこの記事を参考にしてみてください。

Discussion