2022/02/01

祝日を自動反映するカレンダー



1.背景

「国民の祝日」をカレンダーに反映する方法として他サイトなどで良く紹介されているのは、Excelのワークシートに祝日を書き並べ、カレンダー表示時にそのデータを読み取って反映する方法です。この方法は、個人用・会社用な祝日も載せておけばカレンダーに反映されるので素晴らしい方法なのですが、毎回(少なくとも1回/年)祝日を記載する必要があります。
年に1回だけ仕事をするだけなのですが、逆に年に1回だから忘れてしまった なんて事もあるかもしれません。

そこで今回は、祝日データを国のサイトから取得してカレンダーに反映する方法を紹介します。これならばデータを毎年追加する必要が無く、自動的に祝日が反映されることになります。
但し、データは2月に翌年分更新されるということなので、「さて、再来年の祝日は・・・」という確認はできません。

2.システム概要

2-1.祝日データについて

今回のデータは図2-1のように、内閣府の「国民の祝日について」と言うサイトのものを使用します。
祝日データが置いてあるサイト
図2-1

祝日は「国民の祝日に関する法律(昭和23年法律第178号)」というもので決められているようです。その内容は、国民の祝日が〇月〇日という事に加え、その祝日が日曜日だった時には次の平日が休日となったり、祝日で挟まれている日は休日になるなど、結構面倒です。
また春分の日・秋分の日は、国立天文台が天文学に従って毎年2月に翌年の分を発表するそうですし、昨年(2021年)などは東京オリンピックのために、祝日をガラガラ動かしてしまうことだってありました。

1日くらいのズレを許容とすれば、春分の日・秋分の日を含め祝日の日付は一応計算できるのですが、今回は「毎年2月に発表」される「翌年分までの祝日データ」を読み込み、カレンダーに祝日表示をします。そのデータは、図2-1のサイトの下から1/3くらいのところに貼ってある「https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv」というCSVデータです。内容は図2-2のようになっています。
CSVデータ
図2-2

このデータであれば、上記の「祝日が日曜」だったり「祝日に挟まれ」たりして発生する「休日」も含まれていますので、面倒な計算無しにカレンダーに反映できます。
なおデータは1955年分から始まっており、2022年分までで「タイトル1行+祝休日975行+最終行の空白行」の全977行となっています。

2-2.今回のカレンダー

今回紹介する「祝日が自動的に反映されるカレンダー」は、図2-3のような外観です。
今回カレンダー
図2-3

1ヶ月単位の曜日固定で、6行×7列のカレンダーです。上部には年月が表示され、その横のスピンボタンを左右にクリックすることで、±1ヶ月ずつ月が移動します。
そして日曜日は数字が赤字に、土曜は青字になり、祝日・休日は「太字の赤文字」としています。また祝日・休日のセルにはコメントを設定し、マウスを当てることで「国民の祝日・休日名称」が表示されます。

3.プログラムの流れ

祝日付きのカレンダーが表示されるまでの工程は、図3-1のような流れです。
プログラムの流れ
図3-1

システムを起動(=サンプルファイルを開く)すると、まず祝日データを読み込んで「2次元配列データ」を作ります。祝日データは、基本的には内閣府のサーバーに存在しますが、インターネットに繋ぐことが出来ないPCの場合は社内ファイルサーバーに祝日データを置いたり、自分のPC内に祝日データを置くこともOKです。どこから読み込むかは事前に設定しておき、指定された場所からデータを読み込み、配列データを作成します。

次に、カレンダー上部のスピンボタンを操作してカレンダー年月を指定し、その年月のカレンダーの並びを計算します。続いて、土曜日・日曜日の数字を青色・赤色に変更します。
最後に、起動時に作成した祝日の配列データを走査しながら、表示する年月のカレンダー内に「祝日・休日」が存在するかを調べ、存在したら「その数字(=日にち)を赤の太文字」にし、且つそのセルにコメントを作成し祝日名称を書き込みます。

4.標準モジュール(Module1)

今回システムのVBAコードは、標準モジュール・シートモジュール・ワークブックモジュールの3ヶ所に分けて記述しました。
標準モジュールには、祝日データの取り込み部分、シートモジュールにはカレンダーの作成と書式変更の部分、ワークブックモジュールには、シート上のスピンボタンと年月表示のラベルの設定を行っています。

4-1.データ取得先の切替

標準モジュール先頭の宣言部では、図4-1のように変数・定数の宣言をしています。
  1. '========== ⇩(1) 定数・変数の宣言 ============
  2. Public SyukuList As Variant   '←祝日データの配列
  3. Const csvAddress As String = "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv"    '←内閣府HP
  4. 'Const csvAddress As String = "¥¥Server1¥Excel¥Cal¥syukujitsu.csv"       '←ファイルサーバーの例
  5. 'Const csvAddress As String = "C:¥Users¥USER¥Excel¥Cal¥syukujitsu.csv"     '←自PC内の例
図4-1

2行目「Public SyukuList As Variant」は、「祝日データ配列(SyukuList)」の宣言をしています。

4~6行目は「データ取得先(祝日データが置かれている場所)」を定数宣言しています。
4行目「Const csvAddress As String = "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv"」は、内閣府のサイトに置かれている祝日データ(CSVファイル)のURLを記載しています。また祝日データを社内HP上に置く場合も、そのURLを記載して下さい。
5行目「Const csvAddress As String = "¥¥Server1¥Excel¥Cal¥syukujitsu.csv"」は、見え消しにしていますが、祝日データをファイルサーバーに置いている場合のファイル位置を示しています。この場合の理由は以下のような環境が考えられます。
 ・インターネットに繋がらない環境
 ・セキュリティ等を理由にネットに繋がせたくない場合
 ・社特有データも盛り込んだ特殊な祝日データとしている場合
6行目「Const csvAddress As String = "C:¥Users¥USER¥Excel¥Cal¥syukujitsu.csv"」も見え消しにしていますが、自分のPC内に祝日データを置いている例になります。

次に図4-2は、図6-1の242行目から呼び出されます。
今回、祝日データは「インターネット上」または「社内LAN上」「自PC内」等にあるものと想定しています。しかしデータファイルがインターネット上にある場合と、フォルダー名+ファイル名で表される場所にある場合とでは、データの取り出し方法が異なります。
ですので、2つの異なる「祝日データを読み取り、配列データにするプロシージャ」をデータ取得先名で分岐させて実行させるのが、図4-2となります。
  1. '========== ⇩(2) データ取得先の切替 ============
  2. Public Sub makeSyukuList()
  3.  If LCase(Left(csvAddress, 5)) = "https" Then
  4.   Call makeSyukuList1   '←内閣府HP
  5.  Else
  6.   Call makeSyukuList2   '←ファイルサーバー 又は 自PC内
  7.  End If
  8.  If IsEmpty(SyukuList) Then
  9.   MsgBox "祝日データを取得できません"
  10.   End
  11.  End If
  12. End Sub
図4-2

11~15行目は、データ取得先名で「祝日データを読み取り、配列データにするプロシージャ」を分岐させます。
11行目「If LCase(Left(csvAddress, 5)) = "https" Then」は、図4-1の4~6行目で設定したデータ取得先(定数csvAddress)の先頭部分を読み取り、先頭の5文字が「https」であれば「インターネット経由(又はイントラネット経由)」と判断し、12行目「Call makeSyukuList1」で図4-4のmakeSyukuList1プロシージャを呼び出し、「祝日データ読み取りと祝日データ配列作成」を行います。Lcaseで文字列を小文字に変換しているのは、文字比較が確実に行われるようにするためです。

それ以外(ドライブ名(サーバー名)+フォルダー名+ファイル名で表される、ファイルサーバーや自PC内)の場合は、14行目「Call makeSyukuList2」で図4-9のmakeSyukuList2プロシージャを呼び出し、「祝日データ読み取りと祝日データ配列作成」を行います。

寄り道
ここで11行目で「If LCase(Left(csvAddress, 5)) = "https" Then」と、https://に限ってmakeSyukuList1プロシージャ(Web上のデータを取り込む)を実行することについて説明します。逆に言うとhttp://(SSL/TLS暗号化レス)で始まるサイトは、makeSyukuList2側に回されることで、結果的に「祝日データ配列が得られない」ことになります。

今回使用する内閣府HPのデータは「https://」のページに存在します。試しに「https://」を「http://」に変更して接続しようとすると、図4-3のような「httpsへ切り替えて下さい」ページに自動的に飛ぶことになります。そのページには祝日データはありませんので、結果として「祝日データ配列が得られない」ことになります。

これだけの理由でしたら11行目を「If LCase(Left(csvAddress, 4)) = "http" Then」として、makeSyukuList1プロシージャの中でエラーを取得すれば良いのですが、今回makeSyukuList1の中では「エラー有無は、ServerXMLHTTPオブジェクトのStatus値で判断(値200であればOK)」しています。(エラーの原因は、URL記述ミス、通信不良を想定)
しかし内閣府HPをhttp://で接続しても、正式なページが開いてしまうため、Status値が200となってしまい「URL設定ミス」とは判断できないのです。
httpで接続した場合
図4-3

「それならば、データの中身を調べてみて・・・」というチェックの仕方はあるとは思いますが、そこまで祝日データに特化したプログラムにするのであれば汎用性がほぼゼロになってしまいます。それならば「正式なURLはhttps://」と決め打ちする方が良いと考え、先頭が「https」のものだけを「インターネットからデータ取得」としました。

なお「http://」をmakeSyukuList2側で処理すると、ドライブ名(サーバー名)+フォルダー名+ファイル名という形ですらない為にエラーが発生し、「祝日データ配列が得られない」ことになります。
また、http://で接続するとhttps://に自動的に接続し直すサイトもあります。試してはいませんが、その場合には「http://」もmakeSyukuList1側で処理してもOKと思います。

17~20行目は、makeSyukuList1プロシージャ(内閣府HPから取得)、makeSyukuList2プロシージャ(ファイルサーバー、自PC内から取得)で祝日データを取得出来なかった場合、処理を中段させています。
17行目「If IsEmpty(SyukuList) Then」では、祝日データ配列SyukuListが空か否かを調べ、空だった場合は18行目「MsgBox "祝日データを取得できません"」でコメントを表示します。そして19行目「End」で処理を終了します。

もし祝日データ配列(SyukuList)が作られなかった場合(URLやパスの記述ミス、通信不良など)には、祝日が反映されない「単なるカレンダー」として作動します。
また19行目で「End」としていますが、サンプルファイルの様に「makeSyukuListプロシージャ」が図6-1の242行目から呼び出された場合には不要です。但しシステム起動時では無く、例えばカレンダーの祝日の書式設定時(makeSyukuプロシージャ(図5-12)の208行目の「If IsEmpty(SyukuList) = True Then Exit Sub」の代わり)に起動するのであれば、その後の処理を中断させる必要があるためEndステートメントは必要になります。

4-2.インターネット経由で取得

図4-2の12行目から呼び出される「インターネット、イントラネット経由で呼び出されるWeb上の祝日データの呼出し」および「祝日データ配列の作成」をするのが図4-4です。
  1. '========== ⇩(3) ネット経由で祝日データ取得 ============
  2. Private Sub makeSyukuList1()
  3.  Dim httpReq As Object   '←XMLHttpRequestオブジェクト
  4.  Dim buf1 As Variant    '←改行記号で分割した祝日データ
  5.  Dim buf2 As Variant    '←1行の祝日データをカンマで分割したデータ
  6.  Dim i As Long       '←祝日データ配列の行位置
  7.  Dim T As Single      '←接続待ち時間
  8.  Set httpReq = CreateObject("MSXML2.ServerXMLHTTP.6.0")
  9.  With httpReq
  10.   .Open "GET", csvAddress, True
  11.   .Send
  12.   T = Timer()
  13.   Do While .readystate <> 4
  14.    DoEvents: DoEvents
  15.    If Timer() - T > 5 Then Exit Sub
  16.   Loop
  17.   If .Status <> 200 Then Exit Sub
  18.   buf1 = Split(StrConv(.responsebody, vbUnicode), vbCrLf)
  19.   ReDim SyukuList(1 To UBound(buf1, 1), 1 To 2)
  20.   For i = 1 To UBound(buf1, 1)
  21.    If Not buf1(i) = "" Then
  22.     buf2 = Split(buf1(i), ",")
  23.     SyukuList(i, 1) = buf2(0)
  24.     SyukuList(i, 2) = buf2(1)
  25.    End If
  26.   Next i
  27.  End With
  28.  Set httpReq = Nothing
  29. End Sub
図4-4

32行目「Set httpReq = CreateObject("MSXML2.ServerXMLHTTP.6.0")」では、XMLHttpRequestオブジェクトを生成しています。このオブジェクトはサーバーと対話するためのもので、今回はこれを使用してWebからデータを取得します。
寄り道
この「XMLHttpRequestオブジェクト」には、いくつか種類があります。
まず「XMLHTTP」と「ServerXMLHTTP」に分けられます。
「XMLHTTP」オブジェクトの方は、WinInetというAPIを元としており、クライアントアプリケーション用に開発され、長く使われているものです。しかし、WinInetはキャッシュやCookieの管理機能も提供するため、アクセスした情報が利用者の履歴として残ってしまうことがあります。
一方「ServerXMLHTTP」は、WinHTTPというAPIを元としている新しいものです。これはサーバ間のHTTP通信のために開発されたもので、「XMLHTTPのようにキャッシュやCookie などの情報を共有しない」ため、WinInet系よりもセキュリティ面で安全とされています。

また、「XMLHTTP」「ServerXMLHTTP」それぞれにバージョンが有ります。「Microsoft」で始まるものはバージョン1で、「Msxml2」で始まるものはバージョン2以降です。
オブジェクト生成の時にバージョンを指定しないと、最新のものがロードされるはずなのですが、古いものがロードされる可能性もあります。ですので、バージョンも含めて指定した方が確かです(ロードされたことの確認は必要ですが)。

ということで、CreateObjectの際に指定するものは、図4-5の順序で出来るだけ高い優先度のものを使うのが良いと思います。サンプルファイルでは優先度1のものを使用していますが、使用するPCにより使用できるオブジェクトが異なる可能性があります。その際は優先度を下げたものを設定してみて下さい。
なお「優先度の高いものが動くのだから、低いものは必ず動く」という訳でも無さそうです。事実私のPCでは優先度1はロードされますが、優先度2・3・6・8・9はされません。
優先度XMLHttpRequest
1MSXML2.ServerXMLHTTP.6.0
2MSXML2.ServerXMLHTTP.5.0
3MSXML2.ServerXMLHTTP.4.0
4MSXML2.ServerXMLHTTP.3.0
5MSXML2.ServerXMLHTTP
6Microsoft.ServerXMLHTTP
優先度XMLHttpRequest
7MSXML2.XMLHTTP.6.0
8MSXML2.XMLHTTP.5.0
9MSXML2.XMLHTTP.4.0
10MSXML2.XMLHTTP.3.0
11MSXML2.XMLHTTP
12Microsoft.XMLHTTP
図4-5

なお上記以外で、私のPCでは「WinHttp.WinHttpRequest.5.1」も使用できました。この詳細は良くわかりませんでしたが、「安定している」という情報もあります。また単に「WinHttp.WinHttpRequest」と指定してもエラーが出てしまいました。自動的に最適なバージョンをロードしてくれる訳では無さそうです。
また「WinHttp.WinHttpRequest.5.1」を使う際には注意が必要です。プロパティとして「ReadyState」が存在しないようで、接続状態を把握するためにはStatusプロパティを使う必要があります。

34行目「With httpReq」では、生成したXMLHttpRequestオブジェクトを基準に、この後の処理を進めていきます。
35行目「.Open "GET", csvAddress, True」では、Webサーバに対するHTTPリクエストを作成します。このOpenメソッドの構文は「Open(method, url[, async[, user[, password]]])」となっており、5つの引数を設定(第2引数までが必須)します。

第1引数は「HTTPのメソッド」です。主に「"GET"」「"POST"」の2種類ですが、URLの後ろにクエリストリングとして情報を添付した形でリクエストする場合は、"GET"になります。今回は、URL以外に情報を送付する必要がありませんので、"GET"を使用します。
情報をPOST方法で送付する必要がある際は、"POST"を指定することになりますが、その例については「計算や検索を行うサイトからデータを取得する」を参照下さい。

第2引数は、アクセス先のURLです。図4-1の定数csvAddressで宣言したURLになります。

第3引数は、同期・非同期の指定です。非同期通信=True、同期通信=Falseを指定します。
「非同期通信」は、レスポンスを待っている間に他の処理が可能で、他の処理を行っている際にレスポンスを受信すると受信処理を実行するものです。送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式のため、「非同期」と言われます。
一方「同期通信」は、データ通信のリクエストを出してからレスポンスが来るまで、他の処理を行わずにレスポンスを待ち続けるものです。送信するデータ量が非同期通信より少なく、伝送効率が良いのがメリットです。

今回システムでは「非同期通信(第3引数=True)」を使っています。「非同期」の場合は、レスポンスが来る前に次の処理(ResponseBodyの実行(データの受け取り))が行われます。レスポンスが来ていないのにデータを取得しようとすればエラーが出てしまいます。
ですので、レスポンスが来たか否かをDo~Loopなどで確認し、レスポンスが来てから次の処理に移る手法が必要となります。
一方「同期」を使用すれば、レスポンスが来た後にResponseBodyが実行(データの受け取り)されますので、「非同期」のようなDo~Loopで待つような事が不要になります。

一見すると「同期」の方が良さそうですが、待ち続けてしまいハングアップ状態になる可能性があるのと、設定したURLのディレクトリ名やファイル名にミスがあった時にはResponseBodyで得たデータ内容のチェックが必要です。
ですので今回は、安全性およびURL記述ミスなどの処理のやり易さから「非同期」を採用する事にしました。なお他の多くのサイトも「非同期」を推奨しているようです。

なおオブジェクトの種類により、第三引数(同期・非同期の設定)の既定値が図4-6の様に異なるようです。どのオブジェクトを使われても大丈夫なように、明示的にTrueを指定した方が良いと思います。
内容既定(XMLHTTP)既定(ServerXMLHTTP)
True非同期通信
False同期通信
図4-6

第4・第5引数は、Webサーバー接続時にIDとパスワードが必要な場合に設定します。今回は不要ですので省略しています。

36行目「.Send」で、HTTPリクエストをWebサーバに送信します。

非同期ですから、レスポンスが来てから「データ取り込み(responsebody)46行目」をするために、38~42行目ではレスポンスが来たかどうかを確認しながら待ちます。
待つと言っても永遠に待っている訳にもいかないので、待ち時間を測るために38行目「T = Timer()」で、現在時刻を変数Tに保存しておきます。

39~42行目のDo~Loopでレスポンスを待ちます。レスポンスが来ると「readystateプロパティ」が1→・・・→4に変化します。この特性を使って、39行目「Do While .readystate <> 4」でreadystateが4で無い間はDo~Loopを回します。
40行目「DoEvents: DoEvents」では、O/Sへ制御を戻し、Do~Loop中でもESCキーを押すことでコードを中断できるようにします。
41行目「If Timer() - T > 5 Then Exit Sub」では、Do~Loopに入る前(38行目)に計測した時刻に対し、5秒を超えたら「異常」と判断をして処理を中止し、プロシージャを抜け出します。通信が遅い環境の場合は、この時間を調整した方が良いかもしれません。

ここで、URL記述ミス等の種類により、どのような状況になるかを図4-7に整理します。
ミスの内容OpenSendreadystateStatus
1正常OKOK1→4200
2ドメインが違うOKOK1のまま
3サーバー名が違うOKOK1のまま
4ディレクトリ名が違うOKOK1→4404
5ファイル名が違うOKOK1→4404
6https→httpOKOK1→4200
7通信不可OKOK1のまま
8URLでは無いNG
図4-7

この表から、readystateが4にならず41行目で時間オーバーするのは、No.2・No.3・No.7の状態(=ドメインが見つからない等)ということになります。

readystateが4になったとしても、図4-7のNo.4のようにディレクトリ名が間違っている場合もあります。これを引っ掛けるために44行目「If .Status <> 200 Then Exit Sub」を置いています。ディレクトリ名が違っているとStatusが404となりますので、44行目で引っ掛かり処理が中止されます。
(No.6のhttpと記載してしまった場合は、正常と同じ状態になってしまうため、図4-2の11行目でhttps://だけがこのプロシージャに入れるようにしています。)

44行目をクリアすると「正常にデータが取得可能」と判断できます。そこで46行目「buf1 = Split(StrConv(.responsebody, vbUnicode), vbCrLf)」で、responsebodyプロパティを使ってデータを取得し、Unicodeに文字列変換をした後、CrLf(改行マーク)で分割した配列データとして変数buf1に代入します。

ここでresponsebodyで得られるデータはバイト型のため人間の目で判別できるようにUnicode化しています。またデータは図2-2のようにCrLfで改行されて1行1行のデータとなっていますので、CrLfで区切り(Split関数)を入れています。

なおvbLfでSplitしても、vbCrでSplitしても1行ずつの配列となりますが、うまく動作するのはvbLfの方だけです。
その理由は図4-8のように、Crで区切ると「日付データの先頭にLfが付く」のに対し、Lfで区切ると「祝日名の後ろにCrが付く」ためです。日付の先頭にLfが付いていると「日付と認識されない」ためにエラーが出ますが、祝日名の後ろにCrが付いていても「文字列の1つとしてCrが認識される」のです。
ですので、もしvbCrLf以外のデリミタにしたい場合は「vbLf」を使用して下さい。
デリミタの違いでの取り出されたデータの形
図4-8

48行目「ReDim SyukuList(1 To UBound(buf1, 1), 1 To 2)」は、祝日データを入れる配列変数SyukuListのサイズを設定しています。ここで、配列SyukuListの行方向を「1」からスタートさせているのは、タイトル行を省くためです。
データの取り出しは46行目で行っており、Split関数を使ってデータを分割しているため、データを代入した配列buf1は「ゼロ始まりのインデックス」となります。ですので「0 To UBound(buf1, 1)」とすると全データが入るサイズになるのですが、「1 To UBound(buf1, 1)」とすることでタイトル行を除いた全データ部分のサイズとすることが出来ます。
また列方向は、祝日データの1行は「祝日の日付」と「祝日の名称」の2つで出来ていますので、2列分のサイズとしています。

50~56行目では、データの各行を「,(カンマ)」で分割し、「祝日の日付」と「祝日の名称」を祝日データ配列SyukuListの各列に格納していく作業を行っています。今回扱う祝日データは、タイトル行+データ行+改行のみの行 で出来ているので、それに気を付けながら進めて行きます。

50行目「For i = 1 To UBound(buf1, 1)」では、カウンタ変数iをデータ行数分だけ回します。buf1を使用せずに「For i = 1 To UBound(SyukuList, 1)」でもOKです。
51行目「If Not buf1(i) = "" Then」では、操作するデータ行が「空白行(=改行のみ)」以外の時に、52~54行目の処理をしています。「""(長さゼロの文字列)」に対してSplit関数を使ってもエラーは出ないので52行目は通過するのですが、分割が出来ないために53~54行目の配列インデックス指定の部分でエラーが発生することになります。ですので空白行以外での処理としています。
逆に言うと「空白行以外(例えば、スペースのみが入っている 等)」では処理が進んでしまい、カンマで分割できないと53~54行目でエラーが出ることになってしまいます。汎用性を持たせるためには、文字列中にカンマの有無を調べてから処理を進める方が確実かもしれません。

52行目「buf2 = Split(buf1(i), ",")」では、カンマでデータを分割し、配列buf2に格納しています。Splitを使っていますので、インデックスはゼロ始まりです。
53行目「SyukuList(i, 1) = buf2(0)」では52行目で分割した1つ目(インデックス=ゼロ)のデータ「祝日の日付」を祝日データ配列SyukuListの1列目の要素に格納しています。
54行目「SyukuList(i, 2) = buf2(1)」では分割した2つ目(インデックス=1)のデータ「祝日の名称」を祝日データ配列SyukuListの2列目の要素に格納しています。
buf2(0)、buf2(1)を使わずに、Split(buf1(i), ",")(0)、Split(buf1(i), ",")(1)を使用すれば52行目は不要になるのですが、毎回Split関数で計算を行うよりは、一旦変数に入れたものを利用した方が見た目も処理速度的にも有利か と思いこのようにしました。

最後に60行目「Set httpReq = Nothing」で、XMLHttpRequestオブジェクトを解放しています。

4-3.LAN内・自PC内から取得

  1. '========== ⇩(4) LAN内・自PC内から祝日データ取得 ============
  2. Private Sub makeSyukuList2()
  3.  Dim FSO As Object      '←FileSystemObjectオブジェクト
  4.  Dim syukuFile As Object   '←祝日データ
  5.  Dim buf1 As Variant     '←改行記号で分割した祝日データ
  6.  Dim buf2 As Variant     '←1行の祝日データをカンマで分割したデータ
  7.  Dim i As Long        '←祝日データ配列の行位置
  8.  Set FSO = CreateObject("Scripting.FileSystemObject")
  9.  On Error Resume Next
  10.   Set syukuFile = FSO.OpenTextFile(csvAddress, 1)
  11.   If Not Err.Number = 0 Then Exit Sub
  12.  On Error GoTo 0
  13.  buf1 = Split(syukuFile.ReadAll, vbLCrf)
  14.  ReDim SyukuList(1 To UBound(buf1, 1), 1 To 2)
  15.  For i = 1 To UBound(buf1, 1)
  16.   If Not buf1(i) = "" Then
  17.    buf2 = Split(buf1(i), ",")
  18.    SyukuList(i, 1) = buf2(0)
  19.    SyukuList(i, 2) = buf2(1)
  20.   End If
  21.  Next i
  22.  syukuFile.Close
  23.  Set syukuFile = Nothing
  24.  Set FSO = Nothing
  25. End Sub
図4-9

71行目「Set FSO = CreateObject("Scripting.FileSystemObject")」では、FileSystemObjectオブジェクトを生成します。
74行目「Set syukuFile = FSO.OpenTextFile(csvAddress, 1)」では、図4-1で設定した祝日データファイル(定数csvAddress)を開きます。

ここで、ファイルパスの記述ミス等の種類により、74行目のOpenTextFile実行時にどのようなエラーが出るかを図4-10に整理します。
ミスの内容Err.Number=
1正常0
2ドライブ名が違う76
3サーバー名が違う76
4フォルダー名が違う76
5ファイル名が違う53
6httpで始まるURL52
7通信不可76
図4-10

祝日データの設定ミス・通信不良などの時には祝日データを取得できませんので、73行目「On Error Resume Next」でOpenTextFileメソッドでのエラー停止をスルーさせ、75行目「If Not Err.Number = 0 Then Exit Sub」でエラーが発生していたら処理を中止させています。

なお今回は「Set syukuFile = FSO.OpenTextFile(csvAddress, 1)」とオブジェクト変数に代入させる形のコードとしましたが、「With FSO.OpenTextFile(csvAddress, 1)」として、With~End Withの間で処理を行う手法もあります。しかしエラー処理のため、Withステートメントの両端を「On Error ・・・」で挟むのは、あまりカッコ良く無いと思い、オブジェクト変数に一度代入しその両端を「On Error ・・・」で挟む形としました。

またOpenTextFileメソッドの第二引数には、「入力/出力モード」を設定します。図4-11から選択します。
今回は読み取り専用ですので「1(ForReading)」を選ぶしかありませんが、事前に「8(ForAppending)」で開いておき「Listプロパティを使ってファイルの行数を事前取得」することも可能です。
定数内容
ForReading1読み取り専用として開く(既定値)
ForWriting2書き込み専用として開く
ForAppending8開いてファイルの最後に書き込む
図4-11

78行目「buf1 = Split(syukuFile.ReadAll, vbLCrf)」では、ReadAllメソッドを使って祝日データファイル内の「全ての文字列」を読み込み、改行マーク(CrLf)で分割し、配列buf1に格納します。
ここでSplit関数で使っているデリミタは「vbLCrf」ですが、図4-8での説明の通り、間違えて「vbCr」を使用するとその後のコードでエラーが発生しますので注意が必要です。

この後は、図4-4とほぼ同じです。
80行目「ReDim SyukuList(1 To UBound(buf1, 1), 1 To 2)」では、祝日データを入れる配列変数SyukuListのサイズを設定します。行方向を1からスタートすることでタイトル行を除いたデータ部分のみのサイズとしています。
また列方向は、「祝日の日付」と「祝日の名称」の2列分のサイズとしています。

82行目「For i = 1 To UBound(buf1, 1)」では、カウンタ変数iをデータ行数分だけ回します。
83行目「If Not buf1(i) = "" Then」では、操作するデータ行が「空白行(=改行のみ)」以外の時に、84~86行目の処理をします。
84行目「buf2 = Split(buf1(i), ",")」では、カンマでデータを分割し、一旦配列buf2に代入します。
85行目「SyukuList(i, 1) = buf2(0)」では84行目で分割した1つ目(インデックス=ゼロ)の「祝日の日付」データを配列SyukuListの1列目の要素に格納しています。
86行目「SyukuList(i, 2) = buf2(1)」では分割した2つ目(インデックス=1)の「祝日の名称」データを配列SyukuListの2列目の要素に格納しています。

90行目「syukuFile.Close」では、祝日データファイルを閉じています。
92行目「Set syukuFile = Nothing」で祝日データファイルを解放し、93行目「Set FSO = Nothing」でFileSystemObjectオブジェクトを解放します。

また図4-9とは異なる「FileSystemObjectオブジェクトを使わない」方法もあります(図4-12)。
  1. '========== ⇩(5) FileSystemObjectオブジェクトを使わない方法 ============
  2. Private Sub makeSyukuList3()
  3.  Dim buf1() As Variant   '←仮(行と列が逆)の祝日データ配列
  4.  Dim buf2 As Variant    '←1行の祝日データをカンマで分割したデータ配列
  5.  Dim FreeNo As Integer   '←使用可能なファイル番号
  6.  Dim uniRow As String    '←1行の祝日データ
  7.  Dim i As Long       '←データ行位置
  8.  FreeNo = FreeFile
  9.  On Error Resume Next
  10.   Open csvAddress For Input As #FreeNo
  11.   If Not Err.Number = 0 Then Exit Sub
  12.  On Error GoTo 0
  13.  i = 0
  14.  Line Input #FreeNo, uniRow
  15.  Do Until EOF(FreeNo)
  16.   Line Input #FreeNo, uniRow
  17.   If Not uniRow = "" Then
  18.    i = i + 1
  19.    ReDim Preserve buf1(1 To 2, 1 To i)
  20.    buf2 = Split(uniRow, ",")
  21.    buf1(1, i) = buf2(0)
  22.    buf1(2, i) = buf2(1)
  23.   End If
  24.  Loop
  25.  Close #FreeNo
  26.  SyukuList = WorksheetFunction.Transpose(buf1)
  27. End Sub
図4-12

104行目「FreeNo = FreeFile」は、FreeFile関数を使って、使用可能な「ファイル番号」を取得します。FreeFile関数には引数を指定でき、「FreeFile(0)」とゼロを指定すると1~255の範囲の番号を、「FreeFile(1)」と1を指定すると256~511の範囲の番号を返してきます。今回のように引数を省略した場合は「ゼロ」指定と見なされます。
107行目「Open csvAddress For Input As #FreeNo」では、図4-1で設定した定数csvAddressのファイル(祝日データファイル)を読み込みモードで開きます。このForの後ろに設定する「開き方」には図4-13のように5種あります。今回は読み込み(Input)を行います。
キーワードモード処理方法
Input入力モード読込
Output出力モード書込
Append追加モード書込
Randomランダムアクセスモード読込/書込
Binaryバイナリモード読込/書込
図4-13

またAsの後ろに付いている「#FreeNo」は、ファイルを開いてから処理・閉じるまで「どのファイルに対しての処理なのか」が分かるようにするための「ファイル番号」です。基本的には「#印(ナンバー記号)」を付けるのですが、113行目のEOFのカッコ内のように付けてはいけない場合もあります。また付けても付けなくても良い場合もあります。

なお、ここでも図4-10に示したようなファイルパスの記述ミス等があった場合のエラー処理が必要です。そのため106行目「On Error Resume Next」でOpenした際にエラーが発生しても、そのままスルーさせ、108行目「If Not Err.Number = 0 Then Exit Sub」でエラーが発生している際は処理を中止します。
もしエラーが発生した時には「ファイルはOpen出来ていない」ために「#FreeNoは使われなかった」ことになります。ですので108行目のExit Subをする前に、127行目と同様の「Close #FreeNo」を実行することは不要のようです。

祝日CSVファイルを読み込んで祝日データ配列を作っているのは、113~125行目のDo~Loop内です。入力モードでファイルを読み込んでいますので、初期のカーソル位置は先頭にいます。そこから1行読み込んでは、その都度祝日データ配列サイズを大きくしながら、データを格納していきます。
111行目「i = 0」は、その祝日データ配列のサイズの基準点となります。但し102行目でLong型で宣言しているため初期値ゼロとなっており、111行目でわざわざゼロ設定しなくても問題はありません。

112行目「Line Input #FreeNo, uniRow」では、データを取り込む「Do~Loopの外」で、祝日データからまず1行分を読み込んでいます。祝日データの1行目は図2-2の通り「タイトル行」ですので、データ配列としては不要です。ですので「わざとタイトル行を読み込んでデータを捨てる」ことで、祝日データ配列の中に日付以外の行を入れないようにしています。

113行目「Do Until EOF(FreeNo)」では、読み込んだ祝日ファイルを読み終わる(EOF=End of File)まで125行目「Loop」までを繰り返します。
まず115行目「Line Input #FreeNo, uniRow」で1行分の祝日データを読み込み、文字列として変数uniRowに代入します。
117行目「If Not uniRow = "" Then」では、データが代入された変数uniRowの値を調べ、「""(長さゼロの文字列)」以外の場合に118~122行目を実行します。つまり「改行のみのデータ行は無視」することになります。

118行目「i = i + 1」では、カウンタ変数i(=データ行数)を1増やします。
119行目「ReDim Preserve buf1(1 To 2, 1 To i)」は、祝日データ配列の「列数」を変更(118行目でiを1増やしているので、列数を1つ増やすことになる)しています。その際、今まで入れたデータがクリアされないように「Preserveキーワード」を付けています。なお、本当ならば行方向を増やしていきたいのですが、Redimでは最終次元方向(2次元配列の場合は、列方向)しか増やす事ができません。ですのでデータを格納する段階では列方向に増やしていき、完成した後(129行目)で行列を反転させます。

120行目「buf2 = Split(uniRow, ",")」は1行分(祝日の日付+祝日の名称)のデータを「,(カンマ)」で分割し、変数buf2に配列の形で代入します。
そして、121行目「buf1(1, i) = buf2(0)」で1つ目のデータ(祝日の日付)を119行目で1つ増やした配列の1つ目の枠に格納し、122行目「buf1(2, i) = buf2(1)」で2つ目のデータ(祝日の名称)を2つ目の枠に格納します。

全てのデータ行についての処理が完了したら、127行目「Close #FreeNo」で祝日CSVファイルを閉じます。
最後に129行目「SyukuList = WorksheetFunction.Transpose(buf1)」の右辺で、祝日データを格納した配列を行列反転し、左辺のPublic配列SyukuListに代入します。

ファイルサーバー内、自PC内に祝日データファイルがある場合は、図4-9、図4-12のどちらでも処理可能です。ちなみに処理速度もほぼ同等なので、理解し易い方を使っていただければ良いと思います。

5.ワークシート(Sheet1)

5-1.カレンダー表示シート

カレンダーおよび祝日内容を表示するのは、図5-1のようなワークシート上です。横方向には日曜~土曜の7列分を用意し、縦方向は6行分あれば、例え土曜が1日で31日まである月であっても不足することはありません。
カレンダー表示部
図5-1

カレンダーの日付表示部(図5-1のB4~H9セル)の上の3行目には、日~土の曜日を手入力しています。その上には年月を表示するためのLabelを配置します。なお図5-1では、Labelは「外枠あり」「文字の中央揃え」になっていますが、起動時のWorkbook_Open(図6-1)の中でLabelのプロパティ設定をしていますので、ActiveXコントロールで作ったまま でも問題ありません。
その年月Labelの横にSpinButtonを配置し、年月の切り替えが出来るようにしています。

5-2.シートモジュール

5-2-1.定数・変数の宣言

シートモジュールの宣言部では、シートモジュール内で共通で使用する定数・変数の宣言をしています。
  1. '========== ⇩(6) 定数・変数の宣言 ============
  2. Const CalRangeS As String = "B4:H9"
  3. Dim EventStop As Boolean
図5-2

133行目「Const CalRangeS As String = "B4:H9"」は、カレンダーの日付を記入するセル範囲を、文字列として設定しています。標準モジュールで宣言した方が適切かも とも考えたのですが、複数種類のカレンダーをシート毎に作る可能性もあると思い、シートモジュールで宣言しました。
134行目「Dim EventStop As Boolean」は、スピンボタンを操作した際、同じイベントプロシージャを再帰呼び出しして同じコードを実行しないようにするための変数です。「EventをStopさせるフラグ」という意味です。Boolean型ですので初期値はFalseです。

5-2-2.スピンボタンでの年月変更

カレンダー上部の年月変更用スピンボタンを操作した時には、図5-3のChangeイベントが発生します。
  1. '========== ⇩(7) 年月変更のスピンボタン操作 ============
  2. Private Sub SpinButton1_Change()
  3.  Dim LB As String       '←Label1の文字列
  4.  Dim CalDay As Date      '←カレンダーの年月
  5.  Dim CalArray As Variant    '←日付が入ったカレンダー型の配列
  6.  If EventStop = True Then Exit Sub
  7.  LB = Me.Label1.Caption
  8.  If IsDate(LB) = True Then
  9.   CalDay = CDate(LB)
  10.  Else
  11.   CalDay = DateSerial(Year(Now), Month(Now), 1)
  12.  End If
  13.  CalDay = DateAdd("m", Me.SpinButton1.Value, CalDay)
  14.  Me.Label1.Caption = Format(CalDay, "yyyy年mm月")
  15.  EventStop = True
  16.   Me.SpinButton1.Value = 0
  17.  EventStop = False
  18.  CalArray = makeCal(CalDay)
  19.  Me.Range(CalRangeS) = CalArray
  20.  Call clearSyuku
  21.  Call SunSatColor
  22.  Call makeSyuku(CalDay, CalArray)
  23. End Sub
図5-3

142行目「If EventStop = True Then Exit Sub」は、156行目の部分で説明しますが、変数EventStopの初期値はFalseですので、通常であれば条件式(EventStop = True)は成立せずに次のコードに進みます。

144~149行目は、現在表示されているカレンダーの年月日を計算しています。但し何も表示されていない場合は今日を含む年月の初日とします。なお、何年何月のカレンダーが表示されているかは、Label1に表示された「表示カレンダーの年月」から判断をしています。
まず144行目「LB = Me.Label1.Caption」で、カレンダー年月が書き込まれている(はずの)Label1の表示文字列を取得し、変数LB(LaBelのつもり)に代入します。
145行目「If IsDate(LB) = True Then」で、その文字列が日付型であるか否かを調べます。
日付型だった場合は146行目「CalDay = CDate(LB)」で、文字列を日付に変換します。なお、Labelの表示は「2021年12月」などという「年と月だけ」の文字列ですが、これをCDate関数で変換すると「その年月の初日」を戻してくれます。ですので「2021年12月」をCDate変換すると「2021/12/01」という日付が得られます。
Labelの文字列が日付型でなかった場合(例:Labelコントロールを作ったばかりの時は、『Label1』となっている)は、148行目「CalDay = DateSerial(Year(Now), Month(Now), 1)」で、今日を含む年月の1日の日付を計算し、変数CalDayに代入します。

151行目「CalDay = DateAdd("m", Me.SpinButton1.Value, CalDay)」では、144~149行目で得た「現在表示されているカレンダーの年月の初日(CalDay)」に対して、スピンボタンで指示した「1か月前へ」「1か月後ろへ」のどちらかに年月日を移動させます。
SpinButton1のSmallChangeプロパティは初期値1ですので、スピンボタンの左側をクリックすれば-1、右側をクリックすれば+1となります。その変更された値( Me.SpinButton1.Value)だけ「月数(DateAddの第一引数の"m")」を移動させ、その日付を再び変数CalDayにします。これで、新しく表示するカレンダーの年月日が変数CalDayに入ったことになります。
なお、DateAddの第一引数は移動する単位を指定する事になっており、図5-4のように10種ありますが、今回は月を表す「m」を使っています。
 
単位の文字内容
yyyy
m
d
h
n
単位の文字内容
s
q四半期
y通年での日数
w平日
ww
図5-4

153行目「Me.Label1.Caption = Format(CalDay, "yyyy年mm月")」は、「CalDay(新しく表示するカレンダーの年月日)」を「yyyy年mm月」という形の文字列にし、年月表示用のLabel1に書き込んでいます。

この段階では、年月変更用スピンボタンはユーザーによってクリックされ、Value値は-1または+1になったままです。このまま、またユーザーにスピンボタンをクリックされてしまうと「-2・ゼロ・+2 のどれか」になってしまいます。これを151行目の式で実行すると「2ヶ月分移動したカレンダー」等が表示されてしまい、ユーザーの意図とは合わなくなります。
ですので、156行目「Me.SpinButton1.Value = 0」でスピンボタンの位置を中立位置(Value=ゼロ)に戻しています。

しかし、スピンボタンのValue値を変更すると、マクロ側から操作したと言えどもChangeイベントが発生してしまい、図5-3のイベントプロシージャが「再帰呼び出し」されてしまうのです。
再帰呼び出しされたとしても、Value=0ですのでカレンダーの年月が動いてしまうことは無いのですが、同じ処理が重複することになります。ですので無駄を省くために155行目「EventStop = True」で、フラグ変数EventStopを立てる(Trueにする)ことで、再帰呼出しされても142行目「If EventStop = True Then Exit Sub」で、すぐに再帰呼出し先を抜け出してしまうようにしています。
再帰呼出し先を抜け出してきたら、157行目「EventStop = False」でフラグを降ろして(False)おきます。

159行目「CalArray = makeCal(CalDay)」では、図5-5のmakeCal関数プロシージャを呼出し、カレンダーの日付を配列の形にした戻り値を受け取り、変数CalArrayに代入します。makeCal関数の引数には、「新しく表示するカレンダーの年月日(初日)」を指定します。
160行目「Me.Range(CalRangeS) = CalArray」では、自分のシート(サンプルファイルではSheet1)のカレンダー表示セル範囲(図5-2の133行目で定数設定した範囲)に、カレンダー日付の配列を貼り付けます。makeCalから戻されてくる配列のサイズは、カレンダー表示範囲(図5-5の169行目で設定された6行×7列)ですので、図5-2の133行目で設定した定数CalRangeSの範囲は必ず合わせておく必要があります。

ここまでで、カレンダーの日付は完成しています。162~164行目では「土日祝日の色付け」と「祝日名称のコメント化」を行います。
162行目「Call clearSyuku」では図5-9を呼び出し、カレンダー全範囲の文字を標準に戻し、コメントもクリアします。
163行目「Call SunSatColor」では図5-10を呼び出し、土日部分に色付けを行います。土曜日の日付は青色に、日曜日の日付は赤色にしています。
164行目「Call makeSyuku(CalDay, CalArray)」では図5-12を呼び出して、標準モジュール側で作成した祝日データ配列(SyukuList)から該当する日付を探し出し、祝日は赤色の太字にし、祝日名称をコメントとして付け加えます。
makeSyukuプロシージャには2つの引数を渡します。第一引数は「表示するカレンダーの年月日(初日)」を、第二引数には「カレンダーの日付を割り振ったカレンダー型配列」です。カレンダー型配列上の日付を調べて、赤色の太字にするセル位置、コメントを付け加えるセル位置を割り出します。

以上で、祝日データが反映したカレンダーが完成です。

5-2-3.カレンダー配列の作成

図5-3の159行目から呼び出されるのが、図5-5の「カレンダー配列を作成する関数プロシージャ」です。引数として、計算するカレンダーの初日の年月日(CalDay)を受取り、そのカレンダーの配列を戻します。
  1. '========== ⇩(8) カレンダー配列作成 ============
  2. Function makeCal(CalDay As Date) As Variant
  3.  Dim CalArray(1 To 6, 1 To 7) As Variant   '←カレンダー配列
  4.  Dim i As Integer      '←カレンダーの日付
  5.  Dim j As Integer      '←列(曜日)の開始位置
  6.  j = Weekday(CalDay)
  7.  For i = 1 To Day(DateAdd("m", 1, CalDay) - 1)
  8.   CalArray(Int((i + j - 2) / 7) + 1, (i + j - 2) Mod 7 + 1) = i
  9.  Next i
  10.  makeCal = CalArray
  11. End Function
図5-5

169行目「Dim CalArray(1 To 6, 1 To 7) As Variant」は、カレンダー型の配列を宣言しています。日曜~土曜の7列×6行分のサイズです。このサイズは図5-2の133行目の「カレンダーの日付を書き込む範囲」と合わせておく必要があります。

173行目「j = Weekday(CalDay)」は、その月の初日の曜日を取得します。Weekday関数の第二引数を省略していますので、日曜日=1、月曜日=2、・・・、土曜日=7 という数値が得られます。
174行目「For i = 1 To Day(DateAdd("m", 1, CalDay) - 1)」は、指定された月の日付をカウントしています。初日の「1」からスタートし、最終日は「次の月の初日の、1日前の日付」と考えることができます。
引数として受け取ったCalDayは初日の日付ですので、次の月の初日は「DateAdd("m", 1, CalDay)」となります。その1日前「-1」の日付「Day(・・・)」で計算できます。

175行目「CalArray(Int((i + j - 2) / 7) + 1, (i + j - 2) Mod 7 + 1) = i」では、カレンダー配列の各要素の中に日付を格納しています。
まず行位置の数式「Int((i + j - 2) / 7) + 1」を確認していきます。
数式の中の「j」は173行目で求めた「曜日の数値」です。図5-6は、月の初日が日曜(カレンダー配列の左端列)だった場合の例です。この時の月の初日の曜日は日曜日ですので「j=1」となります。このjの値に日にちの数値(174行目のカウンタ変数i)を足したのが図5-6の一番左側になります(〇内の数値)。
カレンダーの行位置の計算(日曜始まり)
図5-6

この一番左の数値(j+i)から「2を差し引いた」のが、図5-6の真ん中の図になります。そして、この真ん中の図の値を「7で割った」のが一番右側の図です。
小数点2桁までを計算していますが、この値の「整数部分」は、「カレンダー配列1行目はゼロ」「カレンダー配列2行目は1」・・・となっているのが分かると思います。ですので「Int((i + j - 2) / 7) + 1」という数式は、カレンダー配列の行位置を表すことになります。

今度は、月の初日が土曜日で始まるカレンダーで確認してみます。上記と同様の計算をしたのが図5-7になります。月の初日の曜日は土曜日ですので「j=7」となります
カレンダーの行位置の計算(土曜始まり)
図5-7

こちらも、計算結果の整数部+1がカレンダー配列の行位置になっていますので、数式は正しい事が分かります。

次に列位置の数式「(i + j - 2) Mod 7 + 1」です。Mod演算子は「整数部分で割り算をした時の余りの値」です。月の初日が日曜日始まりで計算したのが図5-8です。
カレンダーの列位置の計算(日曜始まり)
図5-8

途中までの計算は行位置計算と同じ(左側図と中央の図)です。異なるのは一番右側の図で、「7で割った時の余り値」を見ると、1列目がゼロ、2列目が1、・・・、7列目が6 となっています。つまりその値に+1すれば列位置になるのです。

以上から175行目「CalArray(Int((i + j - 2) / 7) + 1, (i + j - 2) Mod 7 + 1) = i」によりカレンダー配列CalArrayに指定の年月の日付が格納されます。
最後に178行目「makeCal = CalArray」で、カレンダー配列をmakeCal関数プロシージャの戻り値としています。

5-2-4.カレンダー領域の初期化

図5-3の162行目から呼び出される「カレンダー領域の初期化」を行っているのが図5-9です。
  1. '========== ⇩(9) カレンダー領域の初期化 ============
  2. Sub clearSyuku()
  3.  With Me.Range(CalRangeS)
  4.   .Font.ColorIndex = xlAutomatic
  5.   .Font.Bold = False
  6.   .ClearComments
  7.  End With
  8. End Sub
図5-9

カレンダー領域のセル範囲は、図5-2の133行目でCalRangeS定数として宣言しています。このclearSyukuプロシージャは、そのカレンダー領域全体に対しての処理を行っていますので、184行目「With Me.Range(CalRangeS)」でWithステートメントを使ってまとめています。
185行目「.Font.ColorIndex = xlAutomatic」では、文字色を標準(黒色)に戻しています。これは図5-10および図5-12で、祝日と日曜を赤文字、土曜を青文字に変更しているので、まずは初期値の文字色に戻す必要があるからです。
「変更された文字だけ元に戻す」という考え方もあるとは思いますが、1文字ずつ調べるよりはセル範囲全体を元に戻した方が早いと思います。
186行目「.Font.Bold = False」では、祝日設定で太文字にしたものを解除しています。
187行目「.ClearComments」は、祝日名称を表示するためのコメントを削除しています。

5-2-5.土日の文字色変更

図5-3の163行目から呼び出される「土日の文字色を変更」するプロシージャが図5-10です。
  1. '========== ⇩(10) 土日の文字色変更 ============
  2. Sub SunSatColor()
  3.  With Me.Range(CalRangeS)
  4.   .Columns(1).Font.Color = RGB(255, 0, 0)
  5.   .Columns(7).Font.Color = RGB(0, 0, 255)
  6.  End With
  7. End Sub
図5-10

このプロシージャも、カレンダー領域全体を対象にしていますので、195行目「With Me.Range(CalRangeS)」でWithステートメントを使ってまとめています。 196行目「.Columns(1).Font.Color = RGB(255, 0, 0)」は、そのカレンダー領域内の1列目の文字フォントの色を赤色にしています。この中の「.Columns(1)」は、親である「Me.Range(CalRangeS)」の中の1列目という意味です。図で表すと図5-11のようになります。決してExcelワークシートのA列(1列目)という意味ではありません。
カレンダー領域内の相対的列位置
図5-11

197行目「.Columns(7).Font.Color = RGB(0, 0, 255)」も同様に、カレンダー領域内の7列目ですので「土曜日の列」の文字フォントの色を青色にしてます。
なお図5-11でも分かる様に、日付のないセルに対しても文字色変更をしてしまっています。

5-2-6.祝日の書式変更とコメント欄作成

図5-3の164行目から呼び出されるのが図5-12です。引数として、表示カレンダーの年月日初日の日付値(CalDay)と、カレンダー配列(CalArray)を受取ります。なお、プロシージャ内部で使用している「祝日データ配列(SyukuList)」は、図4-1の2行目でPublic変数として宣言されていますので、そのまま使えます。
  1. '========== ⇩(11) 祝日の書式変更とコメント欄作成 ============
  2. Sub makeSyuku(CalDay As Date, CalArray As Variant)
  3.  Dim i As Long     '←祝日データ配列の行位置
  4.  Dim j As Integer    '←カレンダーの列位置
  5.  Dim k As Integer    '←カレンダーの行位置
  6.  If IsEmpty(SyukuList) = True Then Exit Sub
  7.  For i = 1 To UBound(SyukuList, 1)
  8.   If Year(CalDay) = Year(SyukuList(i, 1)) And Month(CalDay) = Month(SyukuList(i, 1)) Then
  9.    For j = 1 To 7
  10.     For k = 1 To 6
  11.      If CalArray(k, j) = Day(SyukuList(i, 1)) Then
  12.       With Me.Range(CalRangeS).Cells(k, j)
  13.        .Font.Color = RGB(255, 0, 0)
  14.        .Font.Bold = True
  15.        .AddComment SyukuList(i, 2)
  16.       End With
  17.       GoTo nextSyuku
  18.      End If
  19.     Next k
  20.    Next j
  21. nextSyuku:
  22.   End If
  23.  Next i
  24. End Sub
図5-12

208行目「If IsEmpty(SyukuList) = True Then Exit Sub」では、祝日データ配列が作成されていなかった場合には、210~229行目の祝日データ処理を中止しています。作成されない時とは、ファイルの場所(URLやパス名)の記述ミスや通信不良などが考えられます。

210行目「For i = 1 To UBound(SyukuList, 1)」で、「祝日データを1行1行」調べていきます。
211行目「If Year(CalDay) = Year(SyukuList(i, 1)) And Month(CalDay) = Month(SyukuList(i, 1)) Then」では、調べる祝日データの日付値「SyukuList(i, 1)」を、表示するカレンダーの日付値(CalDay)と比較し、
 ・年が合っているか:Year(CalDay) = Year(SyukuList(i, 1))
 ・月が合っているか:Month(CalDay) = Month(SyukuList(i, 1))
を調べ、両方とも合致している(=表示カレンダーの祝日データ)場合に、213~224行目を実行します。

213行目「For j = 1 To 7」で列方向を移動し、214行目「For k = 1 To 6」で行方向を移動することで、カレンダー配列内を走査していきます。
215行目「If CalArray(k, j) = Day(SyukuList(i, 1)) Then」で、カレンダー配列の数値(=日付)と祝日データの日付(Day(SyukuList(i, 1)) )が合致した時、その列位置・行位置が祝日ということになるので、216~220行目を実行します。

216行目「With Me.Range(CalRangeS).Cells(k, j)」は、合致した列位置(j)・行位置(k)のセル位置を特定します。
217行目「.Font.Color = RGB(255, 0, 0)」は、そのセルの文字色を赤字にします。
218行目「.Font.Bold = True」は、そのセルのフォントを太文字にします。
219行目「.AddComment SyukuList(i, 2)」は、そのセルに対しコメントを作成し、コメント内容として「祝日の名称」を表示させます。なお今回は、文字高さに合わせてコメント枠高さを変更するという様な見栄え調整は行っていません。

215行目で祝日のセルが見つかったのち、その処理が完了したら、カレンダー内に同じ日付は存在しませんので、引き続きカレンダー内を走査する意味はありません。ですので221行目「GoTo nextSyuku」でFor k~NextとFor j~Nextの2つのFor~Nextを抜け出し、225行目「nextSyuku:」へジャンプすることで、次の祝日データ行に処理を移動させます。

寄り道
なお、祝日CSVデータから祝日データ配列にデータを移す際、改行のみの行についてはデータが入らないためにEmptyとなっています。211行目や215行目では、日付値の年・月・日を取得していますが特にエラーは発生しません。Empty値で日付計算をする時は、Emptyをゼロと扱うようです。
ちなみに、Empty(ゼロも一緒)をVBA上で日付として計算すると「1899年12月30日」です。Year(Empty)は1899になり、Month(Empty)は12、Day(Empty)は30となります。

あれ?と思われた方もいるかもしれません。私もこのサイトで「1900年1月1日をシリアル値の1として日付計算する」と説明してきました。しかし正確には、図5-13のように「セル上で数値を日付として扱うシリアル値は、1900年1月1日を1(図5-13の上側)」とするのですが「VBAでは、1899年12月30日をゼロ(図5-13の下側)」とするのです。
日付のスタート地点
図5-13

そのためVBAでEmpty値を日付換すると「1899年12月30日」となる訳です。
このスタート地点が異なる原因は、遡ればロータス1-2-3の設計時に「1900年はうるう年だと勘違いした」とか「1900年をうるう年とすると計算が楽だった」など、色々な説があるようです。(実際は、1900年はうるう年では無い)
ただし図5-14のように、1900年の3月からは同じ値になるため実害は無いと思います。しかし、ゼロ値を日付として計算したり、ワークシート側とVBA側で日付をやりとりする際には、少しだけ気にしながら作業した方が良いと思います。

日付計算のズレ
図5-14

なお、Emptyの日付計算ではエラーは出ませんが、「""(長さゼロの文字列)」を日付計算に使用するとエラーが発生します。そのため、祝日データ配列SyukuListに「""(長さゼロの文字列)」が入ってしまうと、それを除外する処理が必要となってしまいます。
また、マイナス値の日付はワークシート上ではエラーとなりますが、VBAでは計算してくれます。ワークシート上では日付値がマイナスにならいように気を使いますが、VBAではちょっと気が楽かもしれません。

なお、今回はシステム起動時に「祝日データ配列SyukuList」を作成(図6-1の242行目)していますが、カレンダー上部のスピンボタンを操作した時に作成する方法も考えられます。その際には、208行目の上で「If IsEmpty(SyukuList) = True Then Call makeSyukuList」などのコードを実行すると良いと思います。

6.ブックモジュール(Thisworkbook)

今回システムでは、カレンダーのあるワークシート上に「カレンダーの年月表示用ラベル」と「年月移動用スピンボタン」を配置しています。その2つのコントロールの初期設定をシステム起動時(workbook_open)に行っています。
また「祝日データ配列SyukuListの作成」も同時に行っています。
  1. '========== ⇩(12) コントロールの初期設定と祝日データ取り込み ============
  2. Sub workbook_open()
  3.  Sheet1.SpinButton1.Max = 1
  4.  Sheet1.SpinButton1.Min = -1
  5.  Sheet1.SpinButton1.Value = 0
  6.  Sheet1.SpinButton1.PrintObject = False
  7.  Sheet1.Label1.BorderStyle = fmBorderStyleSingle
  8.  Sheet1.Label1.TextAlign = fmTextAlignCenter
  9.  Call makeSyukuList
  10. End Sub
図6-1

234~237行目はスピンボタンの初期設定、239~240行目はラベルの初期設定です。
234行目「Sheet1.SpinButton1.Max = 1」ではスピンボタンの最大値を1に、235行目「Sheet1.SpinButton1.Min = -1」では 最小値を-1に設定しています。これにより、カレンダー年月は「前後1ヶ月の移動のみ可能」となります。
236行目「Sheet1.SpinButton1.Value = 0」は中立位置をゼロにし、±1ヶ月のどちらでも移動できるようにしています。なおスピンボタンのValue値の初期値はゼロですので、この段階では実際にはValue値に変化はありません。
237行目「Sheet1.SpinButton1.PrintObject = False」は、スピンボタンを「オブジェクトを印刷しない」にします。もしカレンダーを印刷する場合でも、スピンボタンは印刷されない事になります。

239行目「Sheet1.Label1.BorderStyle = fmBorderStyleSingle」は、カレンダー年月のラベルの外枠をON(外枠有り)に設定します。
240行目「Sheet1.Label1.TextAlign = fmTextAlignCenter」は、ラベルの文字列を中央揃えにします。
なお、コントロールの初期設定は、コントロール配置時に手動で行ってもOKですが、何を変更したのか後で分からなくなってしまうため、出来るだけマクロ側から変更するようにしています。

242行目「Call makeSyukuList」は図4-2を呼出し、内閣府HPから、または社内ファイルサーバー、自PCなどから祝日CSVデータを読み込み、祝日データ配列を作成します。

7.最後に

インターネット上には様々なデータが存在しています。今回の祝日データもその1つですが、常に最新の情報にメンテナンスしてくれるのも使う側にとっては魅力的です。最新状態にしておくのは、データをアップデートしている側にとっては結構な負担だとは思います。しかし、そのデータを我々が有効に使ってこそ、アップしている側の苦労が報われるのだと思います。


祝日を自動反映するカレンダー(it-073.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。