ゴミ収集日のリマインダーを作る。

April 10, 2022

はじめに

全ての予定はGoogleカレンダーなどのスマホで確認したい派の人間にとって、いまだに紙に印刷されたスケジュール表という形を採用している「ゴミ収集日カレンダー」は厄介者である。

そこで53cal.jpというゴミ収集日をメールで事前に教えてくれるサービスを用いて、Googleカレンダーに自動でゴミ収集日を登録してもらう、という手段を取っていた。 前日に以下のようなメールが送られてくるので、これをGASで読み取ってGoogleカレンダーに登録するという簡単なものだ。

3/30(水)は、「トレー・プラスチック容器」の収集日です。

ただ、このサービス、以下のメール文を最後にサービスが終了してしまっていた… どうりで最近通知が来なかったわけである。

誠に勝手ながら53calは2022年3月末をもちまして、情報提供サービスを終了させていただきます。これまでのご愛顧に心より感謝申し上げます。
-----
ごみ収集日お知らせサービス53cal(ゴミカレ)

53cal.jpのような代替サービスが他にもあれば助かったのだが、そんなものは今のところ見当たらなかった。 せめてゴミ収集日の情報をAPIで取得できないかも調査したが、少なくとも私が住んでいる地域ではそのようなものは見当たらなかった。

とりあえず、ゴミ収集日のカレンダーを自動的にGoogleカレンダーに登録できさえすればいいので、 その方法を考えることにする。

ゴミ収集日について知る。そして記述する。

私が住んでいる地域ではどうやら、毎週月曜日は紙ゴミの日、などといった感じで、曜日ごとに収集されるゴミが決まっていた。また、1回目の月曜日はペットボトルの日、といった感じで、何回目の何曜日か、というところでも決まっているらしい。

つまり、ゴミ収集日のルールは、以下の3変数で記述できることになる。

  1. ゴミの種類
  2. 曜日
  3. タイミング(1回目, 2回目, … 5回目)

たとえば、毎週月曜日が燃えるゴミの日であれば、ゴミの種類=燃えるゴミ,曜日=月曜日,タイミング=1回目,2回目,3回目,4回目,5回目という感じだ。

ここまで書き下すことができれば、あとは従来通りGASでGoogleカレンダーに追加できるようにJSON形式などにして、それを読み取り自動でカレンダーを追加できるようにすればいい。

"燃えるゴミ": [
            {
                "dayOfWeek": "月",
                "timing": [
                    1,2,3,4,5
                ]
            }
        ],

ゴミの種類によっては、月曜日と金曜日のように週に2回収集するものもあるので、以下のように複数定義されることも想定しておく。(以下は一例である)

"燃えるゴミ": [
            {
                "dayOfWeek": "月",
                "timing": [
                    1,2,3,4,5
                ]
            },
            {
                "dayOfWeek": "金",
                "timing": [
                    1,2,3,4,5
                ]
            },
        ],

ゴミ収集日の定義に従ってリマインダーをセットする。

今回の問題は、JSON形式で定義された、いくつかの曜日とタイミングの入った配列を読み取り、それに対応する日付を得るものと考えていい。

もっと噛み砕いて言えば、特定の月において、月曜日の日付を全て取得してきて、1回目の月曜日だったら、その日付の1番目を返してあげさえすれば達成できる。

以下のステップを踏むことでこれを実装した。詳細はコードを見てもらえればわかると思う。

  1. その月の初日の曜日を取得する
  2. その月の最終日を翌月の1日前の日付を求めて特定し、その月が何日あるのかを把握した
  3. range()を定義してその月の最初の各曜日の日付から、曜日ごとの日付配列を作成した
  4. 作成した曜日ごとの日付配列をゴミの種類ごとに定義したJSONを読み取り、対応する日付を求めた
書いたGASのコード
//https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global*Objects/Array/from
const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (*, i) => start + (i * step))
const weeks = \["日","月","火","水","木","金","土"]
const NOTIFICATION_HOUR = 7
const NOTIFICATION_MINUTES = 30
const PopupReminderTheDayBefore = 720 // 12時間前
let props = PropertiesService.getScriptProperties()
const CALENDAR_ID = props.getProperty('CALENDAR_ID')

function main(){
  let calendars = getDustCalendars()
  for(let \[name,value] of Object.entries(calendars)) {
    Logger.log(name)
    Logger.log(value)
    let calendar = CalendarApp.getCalendarById(CALENDAR_ID)

  value.map(date=>{
    // 開始時刻
    let startDate = new Date(date)
    // 終了時刻
    date.setHours(NOTIFICATION_HOUR+1)
    let endDate = new Date(date)

    if (startDate < new Date()) return
    // 既に登録済みなら終了
    let events = calendar.getEvents(startDate, endDate)
  
    for(var i=0; i<events.length; i++) {
      if (events[i].getTitle() === '[ゴミカレンダー] '+name) {
        return
      }
    }

    let event = calendar.createEvent("[ゴミカレンダー] "+name,startDate,endDate)
    event.setDescription("ゴミカレンダー")
    event.addPopupReminder(5)
    event.addPopupReminder(30)
    event.addPopupReminder(720)
    Logger.log('Event ID: ' + event.getId())
  })
  Utilities.sleep(2000)
  }
}

function deleteCalendars()
{
  let calendars = getDustCalendars()
  for(let \[name,value] of Object.entries(calendars)) {
    Logger.log(name)
    Logger.log(value)
    let calendar = CalendarApp.getCalendarById(CALENDAR_ID)

    value.map(date=>{
      // 開始時刻
      let startDate = new Date(date)
      // 終了時刻
      date.setHours(NOTIFICATION_HOUR+1)
      let endDate = new Date(date)

      // 既に登録済みなら削除
      let events = calendar.getEvents(startDate, endDate)
  
      for(var i=0; i<events.length; i++) {
        if (events[i].getTitle() === '[ゴミカレンダー] '+name) {
          events[i].deleteEvent()
          return
        }
      }
    })
  }
}

// ゴミの種類ごとにゴミ収集日をDate形式で返す
function getDustCalendars() {
  let month = generateMonthMatrixFromToday()
  // definition.json.gsを読み込む
  let def = getJsonForDustCalendar()\[0]

  let dustCalendars = {}
  for(key in def){
    // ゴミの種類ごとに取得する
    let result = def\[key].map(confing=>{
      // 曜日に対応する日の配列を取得する
      let datesFromdayOfWeek = month\[weeks.indexOf(confing.dayOfWeek)]
      // timingに指定された数だけ取得する
      return confing.timing.map(_timing=>{
        // x週目の日付を取得
        return datesFromdayOfWeek.slice(_timing-1,_timing)\[0]
      }).filter(v=>v)
    }).flat().sort((a,b)=>a-b)
    // ゴミの種類ごとにゴミ収集日をDateで保存する
    dustCalendars\[key] = result.map(v=>{
      let date = new Date()
      date.setDate(v)
      date.setHours(NOTIFICATION_HOUR)
      date.setMinutes(NOTIFICATION_MINUTES)
      date.setSeconds(0)
      return new Date(date)
    })
  }
  return dustCalendars
}

function generateMonthMatrixFromToday()
{
  let month = {} 
  let date = new Date()
  const firstDate = getFirstDate(date)
  const days = getLastDate(date).getDate()
  const weekOfDay = firstDate.getDay()

  // 初日の曜日から順番に日付を求めていく
  let _weekOfDay = weekOfDay
  for(let d=1;d<=7;d++) {
    month\[_weekOfDay] = getDateFromWeekOfDays(d, days)
    _weekOfDay += 1
    _weekOfDay = _weekOfDay%7
  } 
  return month
}

// その指定された日のn週間後の日付を月末まで求めて配列で返す
function getDateFromWeekOfDays(numberOfFirstDate,daysOfMonth)
{
  return range(numberOfFirstDate,daysOfMonth,7)
}

// 月初の日付を生成
function getFirstDate(date)
{
  date.setDate(1)
  return new Date(date)
}

// 月末の日付を生成
function getLastDate(date)
{
  date.setMonth(date.getMonth()+1)
  date.setDate(1)
  date.setDate(date.getDate()-1)
  return new Date(date)
}

実際に登録されたカレンダー

まとめ

53cal.jpで今まで得ていたゴミ収集日カレンダーを、サービス終了をきっかけに多少アナログだがゴミ収集日の情報をJSON形式に変換した。 そのデータをもとにゴミ収集日を取得し、GoogleCalendarに登録するGASを書いた。 事象を観察し一般化することで、自動化(プログラム)に落とし込めるということを改めて実感した例だった。


Written by Blackcat ひよっこエンジニア, いつかは自分でサービスを作りたいとずっと言ってる Twitter