自動化は怖くないッ!~Google Apps Script編~

いちいち祝日を調べて手打ちする辛さよ…

最近は脱Excel、Googleスプレッドシートでの各人スケジュール管理が目立ちますよね。
世間に流通しているスケジュール管理ツール等を使えば一発解決したりするものもありますが、大人の事情で中々導入できないことも現実的によくあります。

「独自に作ったGoogleスプレッドシートのスケジュール表、土日はいいけど祝日判定ができないよ!」
「ええっと、今年の祝日は何月何日かな……祝日の情報があるサイトを見て…情報をこの祝日一覧シートに貼り付けて…」
「あれ、この日付もうシートの一覧側に入ってるじゃない!コピペ損だわ!!!」
「かと思ったらこの日付が入ってないよ!」
ハイ、よく見かける光景です。(?)

こんな事を毎年毎年手作業でやるの、本当に大変ですよね。
いっそ手作業やサイトを探すのをやめてしまいましょう!!!

Google App Scriptの出番ですよ奥様!

「いきなりGoogleスプレッドシート以外を使うように指示された!逃げよう!!!」なーんて思わないでくださいね。
その今使っているGoogleスプレッドシートのメニューからマウス一つで使えるようになりますよ!!
上部メニューの「拡張機能」>「Apps Script」をクリックすると、Apps Scriptのページが開きますので、右側のコード領域を全部選択して削除してから後述のコードを全てコピー&ペーストしてください。
(一部buttonClickという名称を使っていますが、後続の手順でボタンから起動するという手順があり、前もって処理名をそのように指定しています。ご了承ください)

//********************************************************* 
// グローバル変数系(変更可能)
//********************************************************* 
// 祝日を一覧化するシート名(無ければ作ります)
const holidaySheetName = "祝日一覧";
// 祝日を書き込む列
const holidayColumn = "A:B";

//*********************************************************
// !!!以降は基本的に変更してはいけません!!! 
// calCsvUrlのみ場合によっては変更可
//********************************************************* 
// 内閣府より提示されている国民の祝日についての情報をまとめたCSVファイルのURL(ほぼ固定)
const calCsvUrl = "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv";
// 祝日を取得するためのURLフェッチオプション(固定)
const urlFetchOption = {
  'method' : 'get',
  'contentType' : 'text/csv',
  'muteHttpExceptions' : true
};

//********************************************************* 
// 公開Function
//********************************************************* 
//********************************************************* 
// 概要:ボタンクリック時処理
//      ボタンクリック時に祝日カレンダー自動取得処理を呼び出す
// 引数:なし
// 戻値:なし
//********************************************************* 
function buttonClick() {
  getHolidayCalendar_();
}

//********************************************************* 
// 非公開Function
//********************************************************* 
//********************************************************* 
// 概要:祝日カレンダー自動取得(メイン処理)
//      ダイアログ表示をし対象年を取得する
//      当年よりも前のデータは非表示にする
// 引数:なし
// 戻値:なし
//********************************************************* 
function getHolidayCalendar_() {
  // 現在表示しているシート
  let dispSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  // 取得対象年
  let targetYear = new Date().getFullYear();
  // 祝日全権取得フラグ
  let allFlag = false;

  let preLastHoliday;       // 取得対象の前祝日
  let response;             // CSV取得のレスポンスオブジェクト
  let csvHolidayData;       // 祝日データ
  let csvHolidayDataLengh;  // 祝日データの量
  let csvHolidayDate;       // CSVファイル内の日付(yyyy/MM/dd)
  let csvHolidayName;       // CSVファイル内の祝日名
  let holidayDate;          // csvHolidayDateをDateオブジェクト変換するための変数
  let formatCsvHolidayDate; // holidayDateをyyyy/MM/dd形式の文字列にした日付
  let strTarget;            // 取得対象年(ダイアログ表示用)
  let holSheet;             // 取得した祝日を保持するシート
  let existHolidays;        // すでに取得済みの祝日データ
  let row;                  // 行番号
  let getCount = 0;         // 取得した件数

  // ボタン起動の場合はダイアログ表示
  targetYear = Browser.inputBox("1955年以降で取得対象年を数字で入力してください。\\n「全て」入力時:1955年から現時点で取得できる全ての祝日を取得\\n空白含む数字以外指定時:本年を自動設定\\n(入力例:" +targetYear + ")", Browser.Buttons.OK_CANCEL);
  // 全角数字は半角数字変換(よくありそうな入力だし)
  targetYear = targetYear.replace(/[0-9]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
    });
  if (targetYear == "cancel") {
    // ×ボタン、キャンセルが押下されたときはユーザが明示的にキャンセルしているはずなので処理終了
    Browser.msgBox("キャンセルされたため処理を終了します。");
    return;
  }
  strTarget = targetYear;
  if (targetYear == "全て") {
    allFlag = true;
    // 全量取得するため現在取得できる最小年を設定
    targetYear = 1955;
  } else if (targetYear == "" || !isFinite(targetYear)) {
    targetYear = new Date().getFullYear();
    strTarget = targetYear;
  } else if (targetYear < 1955) {
    // 不正な値の場合は処理終了(1955年よりも前)
    Browser.msgBox("取得対象年が1955年以前のため、処理を終了します。");
    return;
  }

  // 取得した祝日を保持するシートを設定
  holSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(holidaySheetName);
  // 対象シートが存在しない場合はシート作成
  if (!holSheet) {
    holSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
    holSheet.setName(holidaySheetName);
    // 新しいシートがアクティブ化してしまうので今まで表示していたシートをアクティブ化
    dispSheet.activate();
  }

  // 祝日を一覧化するシートのデータ一覧および最終行
  existHolidays = holSheet.getRange(holidayColumn).getValues().filter(String);
  row = existHolidays.length;

  // そのままではセル側とCSVファイル側で日付形式の差分があるためフォーマットを揃える
  convertDate_(existHolidays);

  // 対象年-1で昨年末の最終祝日を取得
  // 月に11を指定しているが、実際はgetMonthに+1をしたものが対象月(欲しい月:12月)となるため
  preLastHoliday = Utilities.formatDate(new Date(targetYear - 1, "11", "31"),"JST","yyyy/MM/dd");

  // URLアクセスし情報を取得
  response = UrlFetchApp.fetch(calCsvUrl, urlFetchOption);
  if (response == null || typeof response === 'undefined') {
    return;
  }
  // 国民の祝日をまとめたCSVファイルは文字コードがShift-JISなのでエンコードする
  csvHolidayData = response.getContentText("Shift-JIS").split("\r\n");
  csvHolidayDataLengh = csvHolidayData.length;

  // 最終行までの空行よりもCSVファイルの行数が多い場合は行挿入(ループ処理前に実施することで処理速度を遅くしない)
  if (row < csvHolidayDataLengh) {
    holSheet.insertRows(1, csvHolidayDataLengh - row);
  }

  // 取得したCSVファイルの行数だけ実行する
  // 行内容:yyyy/MM/dd,国民の祝日名称
  for (let i = 1; i < csvHolidayData.length; i++) {
    csvHolidayDate = csvHolidayData[i].split(",")[0];
    // ダイアログで入力された対象年でない行はスキップ
    if (!allFlag && !csvHolidayDate.startsWith(targetYear)) { continue; }
    holidayDate =  new Date(csvHolidayDate);
    formatCsvHolidayDate = Utilities.formatDate(holidayDate,"JST","yyyy/MM/dd");
    // 不正な値や過去の祝日データ、同日の場合はスキップ(祝日予定が途中でなくなることはよほどのイレギュラーなのでチェックしない)
    if (holidayDate.toString() === "Invalid Date" || holidayDate < preLastHoliday || existHolidays.toString().indexOf(formatCsvHolidayDate) != -1) { continue; }

    // ここまで来たら取得対象の祝日となる
    row++;  // 行番号
    csvHolidayName = csvHolidayData[i].split(",")[1];
    holSheet.getRange(row, 1).setValue(csvHolidayDate);
    holSheet.getRange(row, 2).setValue(csvHolidayName);
    preLastHoliday = holidayDate;
    getCount++;
  }

  // 日付をソート
  holSheet.getRange(holidayColumn).sort(1);

  Browser.msgBox(holSheet.getSheetName() + "シートへの処理が完了しました。\\n対象年:" +strTarget + "\\n追加した祝日数:" + getCount + "日");
}

//********************************************************* 
// 概要:日付比較用データ整形(Utilities.formatDateを利用)
//      引数で渡されたものに対して処理を実施
//      日付である場合「yyyy/MM/dd」形式に変換
//      日付以外の場合は変換無し
// 引数:values
// 戻値:なし
//*********************************************************
function convertDate_(values) {
  //date型をstringに変換
  for(let i = 0; i < values.length; i++){
    let newValues = values[i].map(
    function(x){
      var type = Object.prototype.toString.call(x);
      if(type == "[object Date]"){
        return x = Utilities.formatDate(x, "JST", "yyyy/MM/dd");
        } else {
        return x;
      }
    });
    values[i] = newValues;
  }
}

そのままキーボードの[Ctrl]キーと[S]キーを同時に押して保存してしまいましょう。

実行してみよう(App Scriptの画面から)

早速、保存したスクリプトを実行してみましょう。
実行する関数が「buttonClick」になっていることを確認し、「▷実行」をクリックします。

初回実行の場合はアクセス承認しなければなりません。とはいっても、ログインしているGoogleアカウントを選択して、画面に表示された内容を確認して「許可」をクリックすればOKです


するとスクリプトが起動しましたね。
ここで、スプレッドシート側に表示を戻してみましょう。

おっおっ、何だかそれっぽいダイアログが出てきました。
何か書いてありますが、何も入力せずにとりあえず「OK」ボタンを押してみましょう。すると…

ダイアログが表示されて、あっという間にスプレッドシートに「祝日一覧」というシートが追加され、そちらに日付と祝日の名称が入力されましたね?よく見ると今年の祝日です。
これはすごい!(自画自賛)

実行してみよう(Googleスプレッドシートの画面から)

今でこそGoogle Apps Scriptのページを表示しているので簡単に実行ができますが、Googleスプレッドシートのメニューからわざわざ選択しなければ実行できないのは不便ですよね。
それならボタンを作って、クリックしたら実行できるようにしてしまえばいいんです!
スプレッドシートの上部メニューの「挿入」>「図形描画」をクリックして、それっぽいものを追加します。
とりあえず私は「祝日取得」ボタンっぽいものを作ってみました。「保存して終了」ボタンを押します。

スプレッドシート上に、さっき作ったボタンっぽい何かが出てきましたね?
そちらをクリックすると右上に「:」っぽいものが出てくるのでクリック>「スクリプトを割り当て」をクリックします。
どのスクリプトを割り当てるか尋ねられますので、さきほどの「buttonClick」を入力して「OK」ボタンを押下します。

さあ、スプレッドシートに配置されたボタンっぽいものをクリックしてみてください。

おっおっ、さっき見たダイアログが表示されました!ボタンっぽいの、ちゃんと動くようになりましたね。
試しに「2000」を入力して「OK」ボタンを押してみましょうか。

終わったみたいです。祝日一覧シートを見てみましょう!

おおお、何だか日付が増えてます。
しかも2022年→2000年の順に祝日情報を取得したのですが、きちんと日付の昇順になっています。
「CSVファイルを開いてコピペと比べれば劣るかもしれません。」
「ううん··コピペより、ずっとはやい‼」

と、ここで注意点

このスクリプトは、あくまでも「国が」設定している国民の祝日の情報を取得し、Googleスプレッドシートに書き込んでいます。
そのため、独自の休み(年末年始やお盆等の長期休暇)がある場合は別途入力する必要があります。
ただし、独自の休みと国民の祝日の日付が被っていても、このスクリプトで二重に出力されることはありませんので、A列に先に独自の休みの日付を追加した上でGoogle App Scriptsを実行すれば、本当に欲しい祝日の情報が揃います。

自動化万歳!!

ほら、Googleスプレッドシートに配置したボタン一つで現時点で国が設定している祝日が取れるようになりましたね!
対象年を設定すれば膨大な祝日情報の中からピンポイントで祝日の情報も取得できます。
逆に全ての祝日情報が欲しい場合も、ダイアログで「全て」と入力してしまえば、重複した日付を除く全ての祝日情報が取得できます。(流石に量が多いので処理は遅いですけどね…)
こういうのって「半角数字じゃなきゃ受け付けません!!」っていうものも多いと思いますが(そうか?)、全角数字を指定しても取れるし、そもそも入力し忘れてOKボタンを押しても今年の祝日は取得できる、というわけです。
ネットから情報をいちいち探し出してGoogleスプレッドシートに1行ずつ書き込むのって本当に面倒くさいですよね。
Let’s enjoy your automatic programming!!