0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Googleカレンダーから祝日情報を取得してみた

Last updated at Posted at 2025-06-07

(参考サイト)
https://excel-ubara.com/GenerativeAI/GAI063.html
(掲載元よりコード引用の許可を頂いています)

祝日の情報を取得したいと思いGoogleカレンダーから取得できないものかと探していたら
「エクセルの神髄」様から丁度探していた記事が見つかった。
ただし特定月の情報のみ取得したかったので月まで指定できるように改変してみた。

年を指定している箇所の特定

parseICS.gs
function parseICS(icsString, targetYearStr) {
(省略)
      if (date && summary && date.startsWith(targetYearStr)) {
        holidays.push({
          date: date,
          name: summary
        });
(省略)
}
  • dateが指定年から始まっていると変数に格納しているのがわかる

date変数がどのように生成されているか確認する

parseICS.gs
function parseICS(icsString, targetYearStr) {
(省略)
      eventLines.forEach(line => {
        if (line.startsWith('DTSTART;VALUE=DATE:')) {
          date = line.substring('DTSTART;VALUE=DATE:'.length).trim();
        } else if (line.startsWith('SUMMARY:')) {
          summary = line.substring('SUMMARY:'.length).trim();
        }
      });
(省略)
}
  • date変数へ格納するのはeventLinesで「'DTSTART;VALUE=DATE:'」から始まるものであるのがわかる
.gs
function parseICS(icsString, targetYearStr) {
(省略)
  const vevents = icsString.split('BEGIN:VEVENT');
  
  if (vevents.length > 1) {
    vevents.slice(1).forEach(veventData => {
      const eventLines = veventData.split(/\r\n|\r|\n/); 
(省略)
}
  • eventLinesはveventDataを改行でsplitしているのがわかる
  • veventDataはveventsをsliceしているのがわかる
  • veventsはicsStringをsplitしているのがわかる
doGet.gs
const ICAL_URL = 'https://calendar.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics';
(省略)
function doGet(e) {
(省略)
  try {
    const icsString = UrlFetchApp.fetch(ICAL_URL).getContentText();
    const events = parseICS(icsString, targetYearStr);
(省略)
}
  • icsStringはGoogleカレンダーの情報であることがわかる

icsStringの変数の中身を確認する

変数の値を出力するコードを書く

doGet.gs
(省略)
function doGet(e) {
    const icsString = UrlFetchApp.fetch(ICAL_URL).getContentText();

    return ContentService.createTextOutput('<holidays>'+icsString+'</holidays>');
(省略)
  • icsStringをxlmで返すコードを追記する

テストデプロイする

01.png

生成されたリンクを実行する

rev02.png

内容を確認する

03.png

  • [BEGIN:VEVENT~END:VEVENT]が単位となっている
  • 日付の指定箇所:[DTSTART;VALUE=DATE:20200101]

オリジナルのコードでは年4桁で渡すが、月まで指定したい場合は
年月6桁にすればよさそうなことがわかった

コードを修正する

年(yyyy)から年月(yyyymm)を取得する

doGet-修正前.gs
function doGet(e) {
  let targetYearStr = '';
  if (e && e.parameter && e.parameter.year) {
    targetYearStr = e.parameter.year;
  } else {
    targetYearStr = new Date().getFullYear().toString(); // 年が指定されていない場合は当年
  }
(省略)
doGet-修正後.gs
  let targetYearMonthStr = '';
  if (e && e.parameter && e.parameter.yearmonth) {
    targetYearMonthStr = e.parameter.yearmonth;
  } else {
    targetYearMonthStr = new Date().getFullYear().toString(); // 年が指定されていない場合は当年
    targetYearMonthStr += (new Date().getMonth() + 1).toString().padStart(2, '0');
  }
(省略)
  • 変数名targetYearStrからtargetYearMonthStr
  • イベント名e.parameter.yearからe.parameter.yearmonth
  • パラメータが指定されていないときは実行月を2桁で取得コードを追記(1月=0が返るGAS仕様なので+1しています)

4桁から6桁のチェックをする

doGet-修正前.gs
(省略)
  if (!/^\d{4}$/.test(targetYearStr)) {
    return ContentService.createTextOutput('<holidays><error><message>Invalid year format. Please specify a 4-digit year (e.g., year=2025).</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }
(省略)
doGet-修正後.gs
  if (!/^\d{6}$/.test(targetYearMonthStr)) {
    return ContentService.createTextOutput('<holidays><error><message>Invalid yearmonth format. Please specify a 6-digit year (e.g., yearmonth=202506).</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }
(省略)
  • 桁数を4から6
  • メッセージを今回の仕様変更に合わせた文言に変更

xmlタグを変更する

convertToXML-修正前.gs
function convertToXML(holidays, year) {
  let xml = '<holidays year="' + year + '">\n';
(省略)
convertToXML-修正後.gs
function convertToXML(holidays, yearmonth) {
  let xml = '<holidays yearmonth="' + yearmonth + '">\n';
  if (holidays.length === 0) {
    xml += '  <message>No holidays found for the specified yearmonth.</message>\n';
(省略)
  • 変数名・タグ名・メッセージ文言yearからyearmonth

全体のコード(JSDocコメントも変更済み)

全体-修正前.gs
// 日本の祝日カレンダーのiCal形式URL
const ICAL_URL = 'https://calendar.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics';

/**
 * Webリクエスト(GET)を処理し、指定された年の日本の祝日データをXML形式で返します。
 * @param {Object} e - イベントオブジェクト。e.parameter.year で年を指定できます。
 * @return {ContentService.TextOutput} XML形式の祝日データ。
 */
function doGet(e) {
  let targetYearStr = '';
  if (e && e.parameter && e.parameter.year) {
    targetYearStr = e.parameter.year;
  } else {
    targetYearStr = new Date().getFullYear().toString(); // 年が指定されていない場合は当年
  }

  // 年が4桁の数字であることを確認
  if (!/^\d{4}$/.test(targetYearStr)) {
    return ContentService.createTextOutput('<holidays><error><message>Invalid year format. Please specify a 4-digit year (e.g., year=2025).</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }

  try {
    const icsString = UrlFetchApp.fetch(ICAL_URL).getContentText();
    const events = parseICS(icsString, targetYearStr);
    const xmlOutput = convertToXML(events, targetYearStr);
    return ContentService.createTextOutput(xmlOutput).setMimeType(ContentService.MimeType.XML);
  } catch (error) {
    Logger.log('Error: ' + error.toString());
    return ContentService.createTextOutput('<holidays><error><message>Failed to retrieve or parse holiday data: ' + escapeXml(error.toString()) + '</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }
}

/**
 * iCalendar形式の文字列を解析し、指定された年のイベントを抽出します。
 * @param {string} icsString - iCalendar形式のデータ文字列。
 * @param {string} targetYearStr - 対象の年(文字列)。
 * @return {Array<Object>} 抽出されたイベントの配列({date: 'YYYYMMDD', name: '祝日名'})。
 */
function parseICS(icsString, targetYearStr) {
  const holidays = [];
  const vevents = icsString.split('BEGIN:VEVENT');
  
  if (vevents.length > 1) {
    vevents.slice(1).forEach(veventData => {
      const eventLines = veventData.split(/\r\n|\r|\n/); // 改行コードの差異に対応
      let date = '';
      let summary = '';

      eventLines.forEach(line => {
        if (line.startsWith('DTSTART;VALUE=DATE:')) {
          date = line.substring('DTSTART;VALUE=DATE:'.length).trim();
        } else if (line.startsWith('SUMMARY:')) {
          summary = line.substring('SUMMARY:'.length).trim();
        }
      });

      if (date && summary && date.startsWith(targetYearStr)) {
        holidays.push({
          date: date,
          name: summary
        });
      }
    });
  }
  return holidays;
}

/**
 * 祝日データの配列をXML文字列に変換します。
 * @param {Array<Object>} holidays - 祝日データの配列。
 * @param {string} year - 対象年。
 * @return {string} XML形式の文字列。
 */
function convertToXML(holidays, year) {
  let xml = '<holidays year="' + year + '">\n';
  if (holidays.length === 0) {
    xml += '  <message>No holidays found for the specified year.</message>\n';
  } else {
    holidays.forEach(holiday => {
      xml += '  <holiday>\n';
      xml += '    <date>' + holiday.date + '</date>\n';
      xml += '    <name>' + escapeXml(holiday.name) + '</name>\n';
      xml += '  </holiday>\n';
    });
  }
  xml += '</holidays>';
  return xml;
}

/**
 * XML特殊文字をエスケープします。
 * @param {string} unsafe - エスケープ対象の文字列。
 * @return {string} エスケープされた文字列。
 */
function escapeXml(unsafe) {
  if (typeof unsafe !== 'string') {
    return '';
  }
  return unsafe.replace(/[<>&'"]/g, function (c) {
    switch (c) {
      case '<': return '&lt;';
      case '>': return '&gt;';
      case '&': return '&amp;';
      case '\'': return '&apos;';
      case '"': return '&quot;';
      default: return c;
    }
  });
}
全体-修正後.gs
// 日本の祝日カレンダーのiCal形式URL
const ICAL_URL = 'https://calendar.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics';

/**
 * Webリクエスト(GET)を処理し、指定された年の日本の祝日データをXML形式で返します。
 * @param {Object} e - イベントオブジェクト。e.parameter.yearmonth で年月を指定できます。
 * @return {ContentService.TextOutput} XML形式の祝日データ。
 */
function doGet(e) {
  let targetYearMonthStr = '';
  if (e && e.parameter && e.parameter.yearmonth) {
    targetYearMonthStr = e.parameter.yearmonth;
  } else {
    // 年月が指定されていない場合は実行時の年月
    targetYearMonthStr = new Date().getFullYear().toString(); 
    targetYearMonthStr += (new Date().getMonth() + 1).toString().padStart(2, '0');
  }

  // 年月が6桁の数字であることを確認
  if (!/^\d{6}$/.test(targetYearMonthStr)) {
    return ContentService.createTextOutput('<holidays><error><message>Invalid yearmonth format. Please specify a 6-digit year (e.g., yearmonth=202506).</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }

  try {
    const icsString = UrlFetchApp.fetch(ICAL_URL).getContentText();
    const events = parseICS(icsString, targetYearMonthStr);
    const xmlOutput = convertToXML(events, targetYearMonthStr);
    return ContentService.createTextOutput(xmlOutput).setMimeType(ContentService.MimeType.XML);
  } catch (error) {
    Logger.log('Error: ' + error.toString());
    return ContentService.createTextOutput('<holidays><error><message>Failed to retrieve or parse holiday data: ' + escapeXml(error.toString()) + '</message></error></holidays>')
                         .setMimeType(ContentService.MimeType.XML);
  }
}

/**
 * iCalendar形式の文字列を解析し、指定された年のイベントを抽出します。
 * @param {string} icsString - iCalendar形式のデータ文字列。
 * @param {string} targetYearMonthStr - 対象の年月(文字列)。
 * @return {Array<Object>} 抽出されたイベントの配列({date: 'YYYYMMDD', name: '祝日名'})。
 */
function parseICS(icsString, targetYearMonthStr) {
  const holidays = [];
  const vevents = icsString.split('BEGIN:VEVENT');
  
  if (vevents.length > 1) {
    vevents.slice(1).forEach(veventData => {
      const eventLines = veventData.split(/\r\n|\r|\n/); // 改行コードの差異に対応
      let date = '';
      let summary = '';

      eventLines.forEach(line => {
        if (line.startsWith('DTSTART;VALUE=DATE:')) {
          date = line.substring('DTSTART;VALUE=DATE:'.length).trim();
        } else if (line.startsWith('SUMMARY:')) {
          summary = line.substring('SUMMARY:'.length).trim();
        }
      });

      if (date && summary && date.startsWith(targetYearMonthStr)) {
        holidays.push({
          date: date,
          name: summary
        });
      }
    });
  }
  return holidays;
}

/**
 * 祝日データの配列をXML文字列に変換します。
 * @param {Array<Object>} holidays - 祝日データの配列。
 * @param {string} yearmonth - 対象年月。
 * @return {string} XML形式の文字列。
 */
function convertToXML(holidays, yearmonth) {
  let xml = '<holidays yearmonth="' + yearmonth + '">\n';
  if (holidays.length === 0) {
    xml += '  <message>No holidays found for the specified yearmonth.</message>\n';
  } else {
    holidays.forEach(holiday => {
      xml += '  <holiday>\n';
      xml += '    <date>' + holiday.date + '</date>\n';
      xml += '    <name>' + escapeXml(holiday.name) + '</name>\n';
      xml += '  </holiday>\n';
    });
  }
  xml += '</holidays>';
  return xml;
}

/**
 * XML特殊文字をエスケープします。
 * @param {string} unsafe - エスケープ対象の文字列。
 * @return {string} エスケープされた文字列。
 */
function escapeXml(unsafe) {
  if (typeof unsafe !== 'string') {
    return '';
  }
  return unsafe.replace(/[<>&'"]/g, function (c) {
    switch (c) {
      case '<': return '&lt;';
      case '>': return '&gt;';
      case '&': return '&amp;';
      case '\'': return '&apos;';
      case '"': return '&quot;';
      default: return c;
    }
  });
}

デプロイする

  • デプロイは同じ手順なので省略する

実行してみる

メッセージ用Xmlも追加した

excel
=IFERROR(FILTERXML(B1,"//holidays/message"),"")
=IFERROR(FILTERXML(B1,"//holidays/error/message"),"")

未指定(2025年06月実行)

11.png
31.png

202505

12.png
32.png

不正な年月

13.png
33.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?