ねこ的アルゴリズム

技術的な話メインの雑記ブログ

分単位指定で定期的に繰り返し実行するための Google Apps Script

GAS で繰り返し実行したいけど、分単位で設定できないの?

f:id:neko_aquaalta:20170214233147p:plain

毎日、毎週、隔週。。 定期的にやってくる業務は、何かと多いものです。

Google Apps Script を使えば、今まで手動でやらなければいけなかった面倒なタスクとはおさらば! 手軽に事務作業を自動化しちゃおう!

そう思って触ってみたはいいものの、あれー?

f:id:neko_aquaalta:20170214235725p:plain

繰り返し実行だと、分単位の時刻指定できないじゃん!

そんなあなたに、

スプレッドシートへ追記するだけで分単位の繰り返し指定ができてしまう

スクリプトをご紹介します。

SPONSORED LINK

Google Apps Script とは?

Google Apps Script は、Google スプレッドシートや Google カレンダー、Gmail といった Google が提供しているサービスと連携できるプログラム実行環境です。GAS では、Javascript を使ってプログラムを書くことができます。

様々な作業を気軽に自動化できる便利ツールで、役割としては Excel のマクロに近いです。

今回は GAS を使って slack(チャットアプリ)に通知する機能を紹介します。GAS と slack の連携をしたことがない方は 初心者がGASでSlack Botをつくってみた などで、送信できるように設定してからお読みいただくとスムーズです。

作ったもの

まず、スプレッドシートを準備してください。(後日公開用のシートを準備します)

f:id:neko_aquaalta:20170214233401p:plain

使い方(仕様)

  • スプレッドシートに追記すると、翌日 1 時に Trigger が自動更新されます。
  • 即日中に飛ばしたければ、スプレッドシートに記入後、 setTodayTriggers を実行してください。
    • 空白行は作らないようにしてください。
  • ※ あまりエラーを想定した作りにはなっていないので、入力ミスると飛ばなくなる可能性が高いです。

入力する項目

  • 最終実行日:
    • 休祝日は指定しても飛びません
    • slack への通知後、自動的に更新されます
    • 周期が「毎日」の場合
      • 適当な日付を入力してください。
    • 周期が「毎週」の場合
      • 次に実行したい日付から 1 週間前を入力してください。
    • 周期が「隔週」の場合
      • 次に実行したい日付から 2 週間前を入力してください。
    • 周期が「毎月」の場合
      • 次に実行したい日付から 1 ヶ月前を入力してください。
      • 2 ヶ月以上先の指定は対応していません。
    • 周期が「月初」の場合
      • 当月でない日付を入力してください。例) 2 月始めに飛ばしたい場合、2 月以外の適当な月の日付を入力する。
    • 周期が「月末」の場合
      • 入力する必要はありません。
  • 周期: 「毎日 / 毎週 / 隔週 / 毎月 / 月初 / 月末」 のうちいずれか
    • 「月初」を指定すると「毎月第一営業日」に飛びます。
    • 「月末」を指定すると「毎月最終営業日」に飛びます。
  • 曜日: 最終実行日と周期を入力すると、自動的に出力されます。
  • 時刻(時間): 通知する時間(h)
  • 時刻(分): 通知する時間(min)
  • チャンネル: チャンネル名
  • 送信者名: 送信者名
  • アイコン: slack に登録されている icon 名
  • テキスト: 送信する文章
  • 最終営業日

ソースコード

かなり適当な部分が多いです。時間があるときにリファクタリングする予定。

// initialize
var now = new Date();
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
var currentHour = now.getHours();
var currentMinute = now.getMinutes();
var ss = SpreadsheetApp.openByUrl("対応するスプレッドシートの URL を記入");  
var sheet = ss.getSheets()[0];
var lastRow = ss.getRange("B:B").getValues().filter(String).length;
var rows = sheet.getRange(2, 1, lastRow - 1, 10).getValues();

function main() {
  rows.forEach(function(row, index, attribute) {
    var lastExeDay = row[0];
    var frequency  = row[1];
    // var weekDay    = row[2];
    var hour       = row[3];
    var minute     = row[4];
    var channel    = row[5];
    var sender     = row[6];
    var icon       = row[7];
    var text       = row[8];
    var ldop       = row[9];
    
    var _frequency = convertFrequency(frequency);
    var diffDays = (today - lastExeDay) / (3600 * 1000 * 24)
    
    // 休祝日の場合 / 指定時刻と異なる場合は skip
    isHoliday = isJapaneseHoliday(today.getFullYear(), today.getMonth() + 1, today.getDate());
    if (currentHour == hour && currentMinute == minute) {
      switch (frequency) {
        case '毎日':
          if (!isHoliday) sendToSlack(channel, sender, icon, text);
          sheet.getRange(2 + index, 1).setValue(today);
          break;
        case '毎週':
        case '隔週':
          if (diffDays == _frequency) {
            if (!isHoliday) sendToSlack(channel, sender, icon, text);
            sheet.getRange(2 + index, 1).setValue(today);
          }
          break;
        case '毎月':
          if (lastExeDay.getMonth() != today.getMonth() && lastExeDay.getDate() == today.getDate()) {
            if (!isHoliday) sendToSlack(channel, sender, icon, text);
            sheet.getRange(2 + index, 1).setValue(today);
          }
          break;
        case '月初':
          if (lastExeDay.getMonth() != today.getMonth()) {
            if (!isHoliday) sendToSlack(channel, sender, icon, text);
            sheet.getRange(2 + index, 1).setValue(today);
          }
          break;
        case '月末':
          if (ldop.toString() == today.toString()) {
            if (!isHoliday) sendToSlack(channel, sender, icon, text);
            sheet.getRange(2 + index, 1).setValue(today);
          }
          break;
      }
    }
  });
}

function sendToSlack(channel, sender, icon, text) {
  var url = "slack の webhook から与えられる URL を記入"
  var data = { 
    "channel": channel, 
    "username": sender,
    "text": text,
    "icon_emoji": icon,
    "link_names": true
  }
  var payload = JSON.stringify(data)
  var options = {
    "method": "POST",
    "contentType": "application/json",
    "payload": payload
  }
  UrlFetchApp.fetch(url, options)
}

/*
 * 今日が日本の休祝日かどうか判定
 */
function isJapaneseHoliday(year, month, day) {
  // 祝日判定
  var startDate = new Date();
  startDate.setFullYear(year, month-1, day);
  startDate.setHours(0, 0, 0, 0);

  var endDate = new Date();
  endDate.setFullYear(year, month-1, day);
  endDate.setHours(23, 59, 59, 999);

  var cal = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  var publicHolidays = cal.getEvents(startDate, endDate);
  var isPublicHoliday = publicHolidays.length != 0;
  
  // 休日(土日)判定
  var isHoliday = [0, 6].indexOf(startDate.getDay()) >= 0;
  
  return (isPublicHoliday || isHoliday);
}

function convertFrequency(frequency) {
  switch (frequency) {
    case '毎日':
      return 1;
    case '毎週':
      return 7;
    case '隔週':
      return 14;
  }
}

function setLastBuisinessDay() {
  rows.forEach(function(row, index, attribute) {
    var frequency  = row[1];
    if (frequency != '月末') return;
    // LastDayOfMonth(今月末)
    ldom = new Date(today.getFullYear(), today.getMonth() + 1, 0)
    var _days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 10 連休まで対応
    _days.some(function() {
      if (!isJapaneseHoliday(ldom.getFullYear(), ldom.getMonth() + 1 , ldom.getDate()) ) {
        sheet.getRange(index + 2, 10).setValue(ldom);
        return true;
      } else {
        // 1 日遡る
        ldom.setDate(ldom.getDate() - 1)
      }
    });
  })
}

function deleteTriggers() {
  var allTriggers = ScriptApp.getProjectTriggers();
  for(var i = 0; i < allTriggers.length; i++) {
      Utilities.sleep(200);
      ScriptApp.deleteTrigger(allTriggers[i]);
  }
}

function setTodayTrigger(hour, minute) {
  // 分単位の指定
  var triggerTime = new Date();
  triggerTime.setHours(hour);
  triggerTime.setMinutes(minute);

  ScriptApp.newTrigger('main').timeBased().at(triggerTime).create();
}

function setTodayTriggers() {
  // すでに設置済みの Trigger を全て削除
  deleteTriggers();
  
  rows.forEach(function(row, index, attribute) { 
    var lastExeDay = row[0];
    var frequency  = row[1];
    var weekDay    = row[2];
    var hour       = row[3];
    var minute     = row[4];
    var ldom       = row[9];
    
    var _frequency = convertFrequency(frequency);
    var diffDays = (today - lastExeDay) / (3600 * 1000 * 24)
    
    if (isJapaneseHoliday(today.getFullYear(), today.getMonth() + 1, today.getDate())) return;
    switch (frequency) {
      case '毎日':
        setTodayTrigger(hour, minute);
        break;
      case '毎週':
      case '隔週':
        if (diffDays == _frequency) setTodayTrigger(hour, minute);
        break;
      case '毎月':
        if (lastExeDay.getMonth() != today.getMonth() && lastExeDay.getDate() == today.getDate()) {
          setTodayTrigger(hour, minute);
        }
        break;
      case '月初':
        if (lastExeDay.getMonth() != today.getMonth()) {
          setTodayTrigger(hour, minute);
        }
        break;
      case '月末':
        if (ldom.toString() == today.toString()) setTodayTrigger(hour, minute);
        break;
    }
  })
  ScriptApp.newTrigger('setLastBuisinessDay').timeBased().atHour(1).everyDays(1).create();
  ScriptApp.newTrigger('setTodayTriggers').timeBased().atHour(2).everyDays(1).create();
}

まとめ

先述の通り、GAS の Trigger は定期的に実行する場合は時間単位での指定しかできません。ただし、 指定時刻 であれば、分単位で実行が可能です。

そこで、毎日 Trigger の再設定をするようなコードにしました。こうすることで、擬似的に指定時刻に実行することが可能になります。

今回公開しているスクリプトは、定期実行時に slack に通知するものになりますが、実行内容を組みかえれば、いろいろなことが可能です。

みなさんもぜひ自動化してみてください。ちんじゅうでした!

SPONSORED LINK

改善項目

  • 同時刻の登録がある場合、Trigger が複数登録されるため修正予定
  • 分単位で指定しており、1 分以上実行が遅れると送信されない厳しめな仕様になっているので、問題があるようであれば修正予定


SPONSORED LINK