見出し画像

個サ作 #18 カレンダーLv.7 後編+α

こんにちは。

前回に続いてカレンダーLv.7を作ります。そこまでできたらLv.7.5に引き上げます。今回で第1章はおわりだ!

参りましょう。


カレンダーLv.7 後編

ループ二重目の実装①

さて、一重目のループは月を周回する処理でした。二重目、三重目はそれぞれ週、日を回すので、これはつまりカレンダーLv.6でやったのと同じなんですよね。

ただし、今回はカウンタ変数を出力位置の指定にも兼用するという使い方はしません。カウンタ変数はカウントするためだけのもの、出力行・出力列は専用の変数が担います

というか、今回は構造的に月・日のカウンタ変数を出力位置として使うことはナンセンスなんですよね。出力する月の数分ループを回すので、そもそも難しいという事情があります。

そして、その出力行・出力列の変数は前回で下記を宣言しています。

    '書き出し行
    Dim writeRow As Integer: writeRow = START_ROW
    '書き出し列
    Dim writeColumn As Integer

「書き出し行」「書き出し列」とコメントしていますが、書き出す場所、と捉えてくださいね。書き始める場所、というニュアンスではなく

週を表すカレンダーの横行は1月につき、最大で6行存在することになります。下図でイメージしてください。これは2024年6月の場合です。

6周あればどんな曜日パターンにも対応できる

ですので、1から6のループとして、以下のように書きましょう。

        '週ループ
        For j = 1 To 6
        
        
        
        Next
赤枠のソースコードを追記してね

カウンタ変数は i の次に j を使います。プログラミング界の慣例です。

月ループでもありました通り、ループが始まった直後に行いたい処理が週ループにもあります。ここまで来たら後は日付を出力するだけかと思いきや、そうではないんですよね。

日ループに入る前にやりたい処理、ということは行単位で行いたい処理ですね。もう少しいうと、各行に共通する要素です。

共通することだから日毎ではなく行に対してやってあげればいい処理があります。さてさて、なんでしょう。

はい、もうお分かり。これは前回もやりました。行幅調整ですね。

すみません汗。これカレンダーLv.6では日ループの中でやってるんですね汗。今回の方が実装の箇所としては適切です。

以下のソースコードを下図のように追記ください。

            '行幅調整
            Rows(writeRow).RowHeight = 20
            Rows(writeRow + 1).RowHeight = 34
赤枠のソースコードを追記してね

はい、ありがとうございます。

(特に意味はないのですが)今この状態で動かすと何が起こるかというと・・・

現在のソースコードで動かしている様子

はい、変数writeRowの値を変えてないので、4行目5行目しか行の高さが調整されないという悲しい動きをします。


ループ二重目の実装②

ここまでで日ループの前の処理は実装できました。あとは7日分出力しきった後にする処理を書きたいと思います。

先ほどの動作確認でお分かりいただいたと思いますが、変数writeRowの値をインクリメントしないことには出力位置が進まないんですよね。

また、7日分出力したあと、ということは変数writeColumnは土曜日の位置にいます。これを日曜日を示す位置に戻してやる必要もあります。

次のソースを下図のように追記ください。

            writeRow = writeRow + 2
            writeColumn = START_COL
赤枠のソースコードを追記してね

変数writeRowに+2をしているのはカレンダーLv.6のForループで「Step 2」としていた部分に該当します。下に向かって2行ずつ移動していくわけです。

はい、この状態で動かしてみるとですね・・・

おぉ!あとはここに各日を出力していくだけだ。フィールドが整った感じがする

いかがでしょうか。変数writeRowの値を動かしたことで、一気にこの後実装するカレンダーのイメージができるようになりました。

月の出力と行の高さ調整が適切に働いていますね。


あともう一つだけ、実装したいことがあります。月ループの時、不要な処理をしないようにと言って下記のようなソースを書きました。

        'エラー対策
        If month > 12 Then
            Exit For
        End If

これと似たようなことで、週の方も不要なループを回避したいと思います。また、週ループは周回が終わるたびに、まだ今の月が続くのか?もう終わる(次の月へ進む)のか?の判定が必要です。

これを網羅する処理を書きましょう。下記を下図のようにお願いします。

            '無駄なループを回避し、次の月へ移行する
            If day > lastDay Then
                writeRow = writeRow + 2
                Exit For
            End If
赤枠のソースコードを追記してね

次の月に進むかどうか?は日をその月の最終日と比較すればできます。

最終日(変数lastDay)の値設定は前回#17でやりましたね。

        '当月の最終日を取得。DateSerial関数の裏技を使う
        lastDay = Format(DateSerial(year, month + 1, 0), "d")

この↑ように月のループ(一重目)に入った直後にやっています。

変数 i は6周しますが、下図のように4周だけで事足りる場合もあります。そんなときに5周6周と回るのはナンセンスですから、すべての日を出力したのか?をチェックし、Trueが返ればループをExitで抜けます

4週間でひと月が終わってしまう例。28日しかない2月だとたまにこういうことがある

その際、変数writeRowに2を足すことで次に「〇月」と出力したい行に進めておくことは忘れないように。

            '無駄なループを回避し、次の月へ移行する
            If day > lastDay Then
                writeRow = writeRow + 2
                Exit For
            End If

この変数writeRowの処理ね。

これでより最適化されました。実は先ほど動作確認してみたときって各月の間がすべて等間隔なんですよね。

でも実際は4行で足りる月、6行要る月、さまざまです。4行で足りる場合、そのあとに無用な空行が続くのは避けたいです。それが今追加した処理で適切なものになりました・・・と思いきやまだなってない

これ、変数dayの値をインクリメントしないと有効にならないですね。変数dayをIfの条件に使っているから。

今、どや顔でもう一度動かしたものをご案内しようとしたらさっきと変わってなくて焦りました。

さて、二重目のループとしてはこれで以上です。

次は核である三重目のループを実装していきましょう。


ループ三重目の実装①

では、早速。下記のソースコードを下図のように実装ください。

            '列ループ
            For k = 1 To 7


            Next
赤枠のソースコードを反映してね

はい、カウンタ変数は i , j , ときたら次は k です。業界の慣習です。

次、このループ内で最初にやりたいことは列幅の調整と1日目の曜日設定です。

まず列幅の調整。こちらは7列に対して1回ずつやりたいのですが、1年間ってだいたい52週間くらいあるじゃないですか。52周で7列に対して毎回調整してると結局それは毎日、つまり365回する羽目になります。

これを7回だけで済むような条件式を組めたらOKです。

では52週から1週だけに絞る条件式ってなんだろう。

  • どこでもいいからひと月に絞る & 7日間出力する1週にしぼる

をすればOKです。下記を下図のようにお願いします。

                If i = 1 And j = 2 Then
                    '1周目は列幅を調整する
                    Columns(writeColumn).ColumnWidth = 10
                End If
赤枠のソースコードを追記してね

こんなソースになりますね。

カウンタ変数 j の指定が1ではなく2な点にご注目ください。1にすると最初の週ということなので、7日間全部を出力しない場合もあります。それを回避したいから確実に7日間は出力する2としています。

3でも4でも同様に動きます。


これに続いて、1日目の曜日設定をしましょう。

1日が何曜日かによって、1週目の日付出力の回数が1~7回のいずれかに決まります。例えば土曜日なら日~金のループは無駄なので避けたいという想いがあるんですね。

ここまでのソースで既に変数firstDayOfWeekに最初の曜日を示す数値を持っています。月ループに入った直後の・・・

        '初日の曜日を取得
        firstDayOfWeek = Weekday(year & "/" & month & "/" & day)

このソースで設定したのですね。設定される値は以下を参考にどうぞ。

画像クリックで該当ページ(公式リファレンス)に遷移する

そして今回はカウンタ変数 k を1から7としているので、この設定値と見事にリンクします。これで、最初の曜日を示す値をカウンタ変数 k に設定してやれば余分なループを回避することができます。つまり・・・!

先ほどの分と併せて以下のソースコードを下図のように追記ください。

                If j = 1 And k = 1 Then
                    k = firstDayOfWeek
                End If
赤枠のソースコードを反映してね

OKです。これでいよいよ日付の出力に入っていけます。


ループ三重目の実装②

では、日付の出力に入ります。

ここはカレンダーLv.6と重複する部分が多いので、一気にソースコードを案内させてください。次のソースを下図のようにお願いします。

                If isDayWritable(year, month, day) Then
                    '値設定
                    dayOfWeek = Weekday(year & "/" & month & "/" & day)
                    Cells(writeRow, writeColumn) = day & "日(" & Left(WeekdayName(dayOfWeek), 1) & ")"
                    'セルの装飾
                    Call cellDecoration(Cells(writeRow, writeColumn))
                    
                    day = day + 1
                    writeColumn = writeColumn + 1
                    Cells(writeRow, writeColumn).Select
                    
                    '無駄なループを回避する
                    If day > lastDay Then
                        Exit For
                    End If
                End If
赤枠のソースコードを追記してね

ありがとうございます。ここは特に説明なしでいけるかな。

ではですね、一度この状態で動作確認をしたいと思います。

現在のソースコードで実行する様子

すみません・・パソコンのスペックの具合かな・・途中でフリーズしましたが、そのまま収録しました。フリーズの時間をカットしたので、実際は上図から受ける印象よりも完了までに時間がかかっています。

さて、できた!という感じがしますね。でもまだ甘いんです。

ためしに、出力開始列を増やしてから実行してみましょう。冒頭にある・・

'1列目からカレンダー開始列までの差分
Private Const BASE_OFFSET_COL = 1

この定数の値です。私は「3」を指定してやってみます。すると・・・

出力列を変更して実行する様子

なにが起きたかと言いますとね、今の状態だとcellDecoration関数とgetDayColors関数はカレンダーLv.6で作ったものを利用しているんですが、あれだと出力位置が2列目からの固定パターンにしか対応していないんです。

だから今、出力列を変えて実行してみるとエラーになっちゃいました。

というわけで、カレンダーLv.7版のcellDecoration関数とgetDayColors関数を作成します。と言ってもやることは既存のものとほとんど同じで要所の値を変えるだけです。

まずはgetDayColors関数から案内させてください。

次のソースコードをwriteMonth関数の下にお願いします。

'セル塗色用の色配列を取得する
Function getDayColors()
    Dim colors(BASE_OFFSET_COL + 1 To BASE_OFFSET_COL + 7) As Long
    colors(BASE_OFFSET_COL + 1) = 15921906
    colors(BASE_OFFSET_COL + 2) = 14998742
    colors(BASE_OFFSET_COL + 3) = 14083324
    colors(BASE_OFFSET_COL + 4) = 15592941
    colors(BASE_OFFSET_COL + 5) = 13431551
    colors(BASE_OFFSET_COL + 6) = 14348258
    colors(BASE_OFFSET_COL + 7) = 15719154
    getDayColors = colors
End Function
赤枠のソースコードを追記してね

ちなみに、カレンダーLv.6ではどんな実装にしたかというと

'セル塗色用の色配列を取得する
Function getDayColors()
    Dim colors(2 To 8) As Long
    colors(2) = 15921906
    colors(3) = 14998742
    colors(4) = 14083324
    colors(5) = 15592941
    colors(6) = 13431551
    colors(7) = 14348258
    colors(8) = 15719154
    getDayColors = colors
End Function

これですよね。まあモジュール「Study5」にあるお手元のものを見ていただいてもいいんですが。

違いは一目瞭然、各インデックスのところに「BASE_OFFSET_COL + 」を付与しています。これをすることで冒頭で指定した定数BASE_OFFSET_COLの値も配列の作りに反映されるというわけです。

じゃあね、試しに実行してみましょう。見ててくれたらいいです。

今の状態で実行する様子

うまくいかへんのかーーい!!

いやね、みなさんはこう思ったかもしれません。「だってまだcellDecoration関数作ってないし」とね。

でもね、cellDecoration関数の作りをよく見ると処理自体はちゃんと動くはずなんですよ、今の対応で。じゃあなにが悪さをしたか?

こういう問題はこの先でも遭遇することがあるかもしれないので、頭の片隅に置いておいてもらえるといいのですが、参照の順序というものがあるんですね。

今、モジュール「Study5」と「Study6」の両方にgetDayColors関数があるのですが、モジュール「Study5」にあるcellDecoration関数から呼ばれたそれはどちらのものが応答したと思いますか?

マクロはモジュール「Study6」に記載した「R_カレンダー7」が動きました。しかし、getDayColors関数を呼び出した関数はモジュール「Study5」に書いています。

先ほどの動作確認ではgetDayColors関数はモジュール「Study5」のものが呼び出されました。

この説明を踏まえて、モジュール「Study5」にあるgetDayColors関数をコメントアウトした上で、再度実行したいと思います。見ててください。

モジュール「Study5」にあるgetDayColors関数をコメントアウトしてから実行する様子

はい、いかがでしょうか。動きましたね。定数BASE_OFFSET_COLに指定した分だけずれた位置から出力できています。D~J列に出してますよね。

今の動作を見てgetDayColors関数がモジュール「Study5」にはなかったからモジュール「Study6」のソースが採用された、という動きはご理解いただけたと思います。

参照の順序というものがあり、呼出し元の近くにあるものから利用される、ということです。

先ほどの動作確認結果をよく見てください。土曜と日曜の文字色が黒のままです。そこの部分はcellDecoration関数が担当しているので、カレンダーLv.7版を作ってあげないとうまく動きません。

では、先ほどのコメントアウトを元に戻したうえで、カレンダーLv.7版のcellDecoration関数をモジュール「Study6」に作りましょう。と言っても、ほとんどLv.6版と同じです。

次のソースコードを下図のようにwriteMonth関数とgetDayColors関数の間に記載してください。

'日付を出力するセルを装飾する
Function cellDecoration(targetRange As Range)

    '塗り替える色の取得
    Dim colors() As Long: colors = getDayColors()

    '罫線設定
    Range(targetRange, targetRange.Offset(1, 0)).BorderAround True
    targetRange.Borders(xlEdgeBottom).LineStyle = xlDot
    'セル背景色設定
    targetRange.Interior.Color = colors(targetRange.Column)
    
    '土日文字塗色
    If targetRange.Column = SUNDAY_COL Then
        targetRange.Font.Color = RGB(255, 0, 0)
    ElseIf targetRange.Column = SATURDAY_COL Then
        targetRange.Font.Color = RGB(0, 100, 255)
    End If
End Function
赤枠のソースコードを反映してね

さて、長々しいソースコードですが、説明したいのは以下の部分のみです。

    '土日文字塗色
    If targetRange.Column = SUNDAY_COL Then
        targetRange.Font.Color = RGB(255, 0, 0)
    ElseIf targetRange.Column = SATURDAY_COL Then
        targetRange.Font.Color = RGB(0, 100, 255)
    End If

前回は数値で指定していた条件式に今回は定数を使用しています。「SUNDAY_COL」と「SATURDAY_COL」の箇所です。

ここまでの流れからするとこの2つの定数も例の定数BASE_OFFSET_COLの値に追随するような内容になっているはずなんですよね。では見てみましょう。ソースコード冒頭へスクロール!

赤枠のソースコードに注目

うおおおおおおおお!!!!!!!!!なっとる!!!

はい、そうなんです。定数BASE_OFFSET_COLの値が変われば自然と定数SUNDAY_COL、SATURDAY_COLの値も変わる、という作りになっています。

定数には演算結果を代入することもできますが、ひとつ注意があります。演算に変数は使えないという点です。変数って値が変わるやつですよね。定数の性格に反してしまうからこれはできません。

さてこれで、実行時に土日の文字色も適切に設定されるはずでしょう。

やってみます。

アンコメントしてから実行している

うおおおおおおおおおおおお!!!!!!でけた!!!!!!

はい、できましたね。

カレンダーLv.7としてはほぼ完成ですが、あと少しだけおつきあいお願いします。


やり忘れたこと、間違えたこと

すみません。ちょっとミスったところとやり残したことがありまして・・・ここでカバーさせてください汗。。

まずひとつめ。

月の境目のこの行あるじゃないですか。ここの行の高さ調整処理が入ってないんですよ。writeMonth関数の最後に次の1行を足してください。

        Rows(monthRange.Row - 1).RowHeight = 20
赤枠のソースコードを追記してね

ありがとうございます。

もうひとつ。まずはこれ↓を見てください。

ちょっとゆっくりめに動くように調整したもの

分かります?セル選択が出力の様子と伴走するんだけど、土曜日のひとつ右のセルにはみ出していますよね。

下記のソースをみてください。

Cells(writeRow, writeColumn).Select

この↑処理が伴走をしています。

今の事象の原因、それは・・・

                    writeColumn = writeColumn + 1
                    Cells(writeRow, writeColumn).Select

変数writeColumnをインクリメントしたあとにSelect処理を使っているからです!泣。やっちまった・・・。

というわけで・・・

↑これを実施してください。

この赤枠のようにすればOK

↑このようになればOK。これでもう一度動かしてみます。

もとの速さでやってる

途中フリーズしたけど・・・汗。でもやりたいことはできてます!OK!

カレンダーLv.7はこれで完成です。おめでとうございます!


最後にもう一声だけいきましょう。

結局このカレンダー、今のままだと誰も使わないですよね。祝日の情報が入ってませんからね。

学習もかねて、祝日情報も出力できるカレンダーを完成させるところまでやっちゃいましょう。


カレンダーLv.7.5

はい、どうしてカレンダーLv.8ではなくLv.7.5にしたのかというと、Lv.7のソースコードに追記するからです。新規には作りません。

まあそんなに大層な処理ではない、という事情もありましてね。

なにをするのかざっくり言いますと、出力する日が祝日だったらその旨をカレンダー中に記載する、です。

で、これをもう少し細かに言うと、処理の内容としては

  • 日付ごとにその日が祝日かどうかをチェックする

    • 祝日なら、祝日情報を出力し次の日に移る

    • 祝日じゃないなら通常の出力で次の日に移る

これだけです。

で、これを読むと大抵の人が「どうやって祝日かどうかをチェックするんだい?」と思ったと思います。

はい、そうなんです。そこ重要ですよね。祝日情報は別途シートを設けてそちらで管理します。

では、いきましょう。


祝日シートを準備しよう

さて、次の動画を参考にシート追加と必要情報の準備をしてください。

この真似をしてね
  • シートを追加

  • シート名を「祝日一覧」に設定

  • 表示タブから「目盛線」をOFF

  • A, B, C列の1行目に「No.」, 「日付」, 「祝日」を入力

  • 全体を選択し、フォントを「BIZ UDPゴシック」に設定

  • そのまま行の高さを18に設定(設定値はなんでもいい)

  • A, B, C列の幅を変更(これも適当でいい)

  • ヘッダを太字にする(上の動画ではやってない)

ありがとうございます。下図のようにできていればOKです。

これが期待値。シート名は[祝日一覧]にしてね

では、続きまして、祝日情報を書いていきます。以下にアクセスください。

下図のような祝日一覧があるので、これを今作ったシートにもってきましょう。

以下の動画を参考にしてください・・・と言いたいのですが、ファイルサイズの都合かアップできませんでした・・。参照される方はお手数ですが、ファイルをダウンロードください。

下図のようにできたらOK。

これが期待値

月日情報をセルにコピーすると自動で「2023/」がついてしまいます。取得元は2024年についての情報なので、「2023」を「2024」に一括置換しています。

ちょっとまった。これ↑たぶんあなたがされるときは問題にならない。なぜなら2024年だろうから

動画中で触れていないですが、「A2」セルを選択した状態で・・・

「ウィンドウ枠の固定」をクリックするとヘッダ行がスクロールに追随せずに固定表示させることができます。けっこう便利です。

これで事前準備が整いました!ソースコードの実装に入りましょう。あと少しです。


ソースコードを実装するでごわす①

さて、処理内容について先ほど

  • 日付ごとにその日が祝日かどうかをチェックする

    • 祝日なら、祝日情報を出力し次の日に移る

    • 祝日じゃないなら通常の出力で次の日に移る

と伝えております。

ですので、これに倣いある日付が祝日かどうかをチェックする準備をします。チェックをするにはまず、ソースコード上で「これが祝日だ」という情報を保持している必要があります。

それをやりましょう。

あ、カレンダーLv.7.5と謳っていますが、マクロ名称は「R_カレンダー7」のままでいきます。

次のモジュールレベル変数を下図のように追加ください。

'祝日配列
Private nationalHolidayArray() As Variant
赤枠のソースコードを反映してね

はい、配列を用意しました。これに祝日情報を格納しようと思います。これは最初に一度だけすればよい処理ですね。

最初に一度だけする処理と言えば・・・?

そうです。calendarInit関数です。

次のソースコードを下図のように挿入ください。

    '祝日設定関連処理
    With Sheets("祝日一覧")
        Dim lastRow As Integer: lastRow = .Cells(Rows.count, 2).End(xlUp).Row
        nationalHolidayArray = .Range(.Cells(2, 2), .Cells(lastRow, 3)).Value
    End With
赤枠のソースコードを反映してね

はい。「Sheets(シート名)」という書き方は初出でしょうかね。

これまでは処理を実行するときに開いているシートが実行対象のシートだったので、特に意識することはなかったのですが、今回は初めて別のシートの内容を参照する、という用があります。

そういうときは「Sheets(シート名)」という書き方をして特定のシートを指定する形で書きます。今回はWith句を使っていますが、これはつまり・・

Dim lastRow As Integer: lastRow = Sheets("祝日一覧").Cells(Rows.count, 2).End(xlUp).Row
nationalHolidayArray = Sheets("祝日一覧").Range(Sheets("祝日一覧").Cells(2, 2), Sheets("祝日一覧").Cells(lastRow, 3)).Value

と、書いているのと同じです。ながい!

「Sheets("祝日一覧").」の後はこれまで散々書いてきたように「Cells~」と続ければいいだけです。

過去#5でもお伝えしていますが、プログラミングにおいて要素と要素をつなぐ「.」ドットは「に所属する」と解釈いただければいいので、今回で言うなら「祝日一覧」シートに所属するXXセル、というように捉えることができます。

もう一度、ソースコードを見てみます。

    '祝日設定関連処理
    With Sheets("祝日一覧")
        Dim lastRow As Integer: lastRow = .Cells(Rows.count, 2).End(xlUp).Row
        nationalHolidayArray = .Range(.Cells(2, 2), .Cells(lastRow, 3)).Value
    End With

これも初出なのですが、↓この部分。

.Cells(Rows.count, 2).End(xlUp).Row

これは代入先の変数名(lastRow)から想像がつくかもしれませんが、今回で言うなら・・・

祝日シート上の最終行、「22」という数字が代入されます。

これはですね、まず「Rows.count」が全行の数をもっています。

.Cells(Rows.count, 2).End(xlUp).Row

つまり

1048576!!という値を持っています。つまり

.Cells(1048576, 2).End(xlUp).Row

1048576行目の2列目を選択しています。そこから上方向に移動します。

移動の仕方は・・・[Ctrl]キーを押しながら上キー!

これをするとですね、次に値のあるセルまで一気にワープします。

ほらね。で、ソースに戻ると・・・

Dim lastRow As Integer: lastRow = .Cells(Rows.count, 2).End(xlUp).Row

「End(xlUp)」で到達したセルの「.Row」つまり行番号を取得しています。

これで変数lastRowには「22」が入る、というわけです。続いて

    '祝日設定関連処理
    With Sheets("祝日一覧")
        Dim lastRow As Integer: lastRow = .Cells(Rows.count, 2).End(xlUp).Row
        nationalHolidayArray = .Range(.Cells(2, 2), .Cells(lastRow, 3)).Value
    End With

先ほどモジュールレベル変数として宣言した変数nationalHolidayArrayに対して、セルのとある範囲を代入していますね。その範囲とは・・・

変数lastRowの部分を先ほど取得した「22」に置き換えると

nationalHolidayArray = .Range(.Cells(2, 2), .Cells(22, 3)).Value

こういうことになります。2行目2列目つまりB2セルから22行目3列目つまりC22セルまでの範囲を変数nationalHolidayArrayに代入しました。

ここでおそらく思われたのが「セル範囲って変数に代入できるんですね」、というところでしょう。

変数nationalHolidayArrayの宣言部をもう一度見てみると・・・

'祝日配列
Private nationalHolidayArray() As Variant

データ型が「Variantヴァリアント」となっています。これはなんでも来いという型なんですね。なので文字列はもちろん数値、真偽値、オブジェクトなんでもOKです。

これで言うと、今回の代入はRange型というオブジェクトです。

では、入った様子を見てみましょう。

・・・と、すみません。モジュールレベルで宣言した変数はローカル変数に現れないのを思い出しました。そのためWith句の中に以下の2行を追加し、この変数tempArrayで見てみます(見てるだけでいいですよ)。

        Dim tempArray As Variant
        tempArray = .Range(.Cells(2, 2), .Cells(lastRow, 3)).Value
配列の中身をローカルウィンドウから確認する様子

はい、2次元配列として祝日情報を保持していますね。

このようなセルに書いた情報をそのままプログラムで保持させたいときに、今回の手法はよく使います。セル範囲を指定して変数に代入する、という方法ね。

ひとつだけある注意点も一緒にお伝えしていきましょう。

最初の添え字が「1」になることを伝えたい図

#10のときに配列の添え字は「0」から始まるといいました。ただし、このようにセルの範囲からガバっと作った場合、「1」が最小の添え字になるんです。お間違え無きよう。

#14で配列の添え字最小値を0か1どちらかに指定できるって

この↑リファレンスと一緒にお伝えしましたが、今回のケースではそれとは関係なく1からになります。

はい、ではプログラミング上での祝日情報の準備も完了です。

さっきのおためしソースを書いて確認してくれた方はそのソースは消しておいてくださいね。


ソースコードを実装するでごわす②

それではいよいよ祝日情報をカレンダーに盛り込むための実装です。

1日1日、きみは祝日なのかい?と検証するわけですからもう実装箇所はお分かりですね。日付の出力箇所です!

次のソースコードを下図のように追記ください。

'祝日設定
Call setNationalHoliday(Cells(writeRow, writeColumn), year, month, day)

はい、新しい関数の登場です。これを実装し、動作確認をしたらもう終わりです。

今、呼び出し側を先に書いたので、呼び出されるsetNationalHoliday関数を実装しましょう。

次のソースコードを下図のように追記ください。場所はgetDayColors関数の後でよいです(後から追加したもの、おまけという意味があるのでね)。

'祝日情報を設定する(https://syukujitsu.com/2024.html)
Function setNationalHoliday(writeRange As Range, year As Integer, month As Integer, day As Integer)
    '対象の年月日をDate型で取得
    Dim targetDate As Date: targetDate = DateSerial(year, month, day)

    '祝日を取得する(あれば値、なければブランク)
    Dim holiday As String: holiday = lookup(targetDate, nationalHolidayArray, 1, 2)

    If holiday <> "" Then
        writeRange.Offset(1, 0).Value = holiday
        writeRange.Offset(1, 0).Font.Color = RGB(255, 0, 0)
        writeRange.Font.Color = RGB(255, 0, 0)
    End If
End Function
赤枠のソースコードを反映してね

この連載、まったくの素人の方を対象にはじまり現在#18まで来たわけですが、ここまで懸命に取り組んでいただいたあなたならソースコードを読んだらだいたい何をしてるのか、おわかりいただけるだろうと思います。

ですので、薄味で進行しますね。まずこちら。

    '対象の年月日をDate型で取得
    Dim targetDate As Date: targetDate = DateSerial(year, month, day)

変数year, month, dayをDateSerial関数に渡して、その戻り値を変数targetDateに代入しています。変数targetDateの方はDateデート型。

DateSerial関数は#17で説明しています。

これは年月日情報を日付型でほしいからやっている処理です。なぜ日付型である必要があるかというと、比較対象の日付情報も日付型で書いているからです。

祝日の日付を日付型で設定していることを示す図

[祝日情報]シートの中身を用意するときに、日付欄は書式を「日付」にしました。この年月日を日付型で取得する処理と繋がっています。

次の処理がこれです。

    '祝日を取得する(あれば値、なければブランク)
    Dim holiday As String: holiday = lookup(targetDate, nationalHolidayArray, 1, 2)

先ほど取得した日付型の変数と祝日情報を使ってその日付が祝日なのかどうかをここで取得しています。祝日じゃなかったら空文字が返ってきます。

ここのlookup関数は自作関数なのでこの後案内します。

さて最後!

    If holiday <> "" Then
        writeRange.Offset(1, 0).Value = holiday
        writeRange.Offset(1, 0).Font.Color = RGB(255, 0, 0)
        writeRange.Font.Color = RGB(255, 0, 0)
    End If

変数holidayに値があればそれすなわち祝日だということです。

ですので、その場合はセルに文字色をつけ、祝日情報を代入する処理を実施します。文字色は日付と祝日の文字色どちらにもやりましょう(セルが別です)。

さて、あとはlookup関数を実装すれば動作確認をして終わりです。


ソースコードを実装するでごわす③

次のソースコードを下図のように追記ください。場所はsetNationalHoliday関数の後でOKです。

'2次元配列から値を検索する
Function lookup(searchValue As Date, searchArray As Variant, searchColumn As Integer, fetchColumn As Integer)
    For i = LBound(searchArray) To UBound(searchArray)
        If searchArray(i, searchColumn) = searchValue Then
            lookup = searchArray(i, fetchColumn)
            Exit Function
        End If
    Next
End Function

はい、lookup関数です。こちらは引数が4つでして

  • 探したい値

  • 検索対象の配列

  • 配列中の探したい列

  • 値が見つかったときに取り出す列

という構成になっています。呼び出し元と併せてみてみましょう。

    '祝日を取得する(あれば値、なければブランク)
    Dim holiday As String: holiday = lookup(targetDate, nationalHolidayArray, 1, 2)

まず1つ目の「探したい値」には変数targetDateを渡しています。その日付を使って祝日かどうかを探したいわけなので、そうですね。

そして2つ目の「検索対象の配列」には変数nationalHolidayArrayを渡しています。これはモジュールレベルで宣言しているからわざわざ引数にしなくてもアクセスできます。では、なぜこのような実装にしているか。

使いまわすときにこの方が便利だからですね。汎用的な作りにしています。

そして3つ目、「配列中の探したい列」です。ここには1を指定しています。これは何かというと探したいものが日付なので当然日付情報を持っている添え字を指定することになります。そしてそれは・・・

探したい列を「1」にしたら祝日の日付情報になることを示す図

これを見るとわかりますね。日付を持っているのは配列の各要素における1列目です。正確には2次元目の1要素目です。

では最後の「値が見つかったときに取り出す列」には固定値で2を指定していますが、祝日情報が欲しいので・・・

探したい列を「2」にしたら祝日の名前情報になることを示す図

2列目からの取得する、ということですね。


4つの引数の役割については理解できました。では実際にソースコードとしてはどのように書いているのか、見てみましょう。

'2次元配列から値を検索する
Function lookup(searchValue As Date, searchArray As Variant, searchColumn As Integer, fetchColumn As Integer)
    For i = LBound(searchArray) To UBound(searchArray)
        If searchArray(i, searchColumn) = searchValue Then
            lookup = searchArray(i, fetchColumn)
            Exit Function
        End If
    Next
End Function

やってることは結構地味でして、配列の中から一つずつチェックし、第一引数である検索値と第3引数を使って取得する検索対象の値が一致していたら、第4引数から取得できる値を呼び出し元に返す、という動きです。

LboundとUBoundは#10でも出てきましたね。配列の最初の要素から最後の要素まで走査するときによく使います。

2次元配列なので

searchArray(i, fetchColumn)

という風に1次元目と2次元目の添え字を渡しています。少し難しく見えますがやってることはこれまで何度も見てきた

Cells(i, fetchColumn)

Cellsと同じです。要は行、列を指定するだけのことです。今回はセルの塊を配列化したのでより分かりやすいと思います。

実装は完了しました。いよいよ最後の動作確認です!


動作確認でごわす

例によってまたフリーズするかもしれませんが・・・

'出力する月数
Private Const WRITE_MONTHS = 12

モジュールレベルで宣言している出力する月数の定数には「12」を指定してやります。

で、で・・・でけた~~~~~!!!!!!

これにてカレンダーは完成!実用性もある!

あとは煮るなり焼くなり好きにしてくれ!!

まだ第1章が終わっただけだけど・・・。これで多くの優秀なプログラマーを生み出す土台ができた・・・!!

もちろん趣味もヨシ!週末プログラマー上等!幽霊エンジニア上等!


今回の補足

定数の性格

カレンダーLv.7ではたくさんの定数を使っていますよね。

前回#17で行った定数の説明で「引数みたいに使うことができ、保守性や可読性の向上に貢献するものだよ」と伝えたのですが、ちょっと混乱を招いているかも、と思ったので簡単に整理させてください。

「定数」と一口に言っても用途や性格が微妙に異なっています。

こんな感じ。

まあそんなに厳密に区分けしようとしなくてもいいです。混乱されてる方がいるかも、と思って補足しただけですのでね。


IsDate関数にみるプログラミング初学者あるある

突然ですが、下図をご覧ください。

上記の公式リファレンスから一部抜粋

カレンダーLv.4にてisDayWritable()関数を実装いただきました。それは渡された年月日情報から有効な日付ならTrueを、無効な日付ならFalseを返す、というものでした。

実はもうあるんです。VBAに。

今回はまあ学習の目的があったのでね、自作しましたが、検索したり調べたりするのが下手なうちは頑張って作った関数が既に用意されている、ということはちょいちょいあります。

それもあって、たまに検索の仕方のことをお伝えしました。検索できる、欲しい情報に辿り着く力、めちゃくちゃ大事です。

ピンポイントで欲しいものがなかったとしても近しい情報は案外転がっていたりするものです。見つける以外に情報を咀嚼する、加工する力も付けられるとGoodですね。


後悔してる変数名・関数名

私のボヤキみたいなものですが、もう最後なのでね、言わせてください。

変数名ね、まあなんでもいいっちゃなんでもいいんですが、読み手のことを考えると丁寧につけないといけません。

↑この辺、読んでもらうとなんとなくわかるんですが、関数名は「動詞 + 名詞」にしようとか、変数名は基本的に「名詞」のみとか、だいたいのルールがあるんです。

それらの事情を汲むとですね・・・

カレンダーLv.4で作ったisDayWritable関数・・・せめてisWritableDay関数にすべきだったな、とかカレンダーLv.6で作ったcalendarInit関数・・・initCalendar関数にすべきだったな、とか・・・思います。

変数、定数、関数にいかに適切な名称を与えるかというのはプログラマにとって永遠の課題なんですよ。だから私も偉そうに講釈を垂れつつも「(どうか粗を指摘しないでくれ・・・!)」と思っています。

びくびくしています。

ですので、この先もあらゆる場面で変数や定数、関数が登場しますが、どうか優しく見守ってください。英語が得意という方はその自信でもってして、より最適だと思われる命名をしていただけたらOKです。

だいたい英語苦手ですからね、私。勉強しろって話なんですけども・・・笑


マクロの効能

もういよいよおわりだぞ というところなのですが、改めて決まった事務処理や手順をマクロ化することのメリットをお伝えさせてください。

マクロ化とはつまり、人が手作業ですることをプログラムにやらせることだと捉えてください。

これは作業の属人性を排除する効果があります。

同じ作業でも人によって結果や仕上がりが異なる、だと困る場面もでてくるでしょう。プログラムを汲んでマクロを実行する形にしておくと誰がやっても同じ結果に仕上がるようになります。

再現性がある、と言い換えることもできます。実際のお仕事として考えたら引継ぎも楽ですよね。ボタン一つ押すことだけ伝えたらいいわけですから(もちろん業務に応じてその前後の作業はあるでしょうけれど)。

また、本編でも出てきましたが、少々の変更であればパラメータ化しておくことで対応できます。この辺りの恩恵は第3章でも感じていただく機会がありますので、乞うご期待です。

今回はどれも一瞬で終わる機能ばかりでしたが、処理対象のデータやファイルが多くなったり、処理そのものが複雑になると一度実行してから完了するまで1時間かかることも珍しくありません。

そうなると、(仕事においては)お昼休みの間にマクロにやらせておくだとか、なんなら業務中でもマクロに作業をやらせてその間自分はサボっておくリフレッシュしておくだとか、いろいろやりようがあるんですね。

作る段階では大変かもしれませんが、使いまわしや応用が利く場合はとてもオススメです。

その際、作った当人にしか仕様や動作が分からない、なんてことにせず、本人不在時や後継者のことも考慮した内容で作成できていればもう言うことなしです。


おわりに

個人サイトの作り方 第一章終わりです。

お疲れさまでした。自信持っていただいていいです。難しいことをやり遂げました。誰にでもできることじゃありません。ぶっちゃけきつかったと思います。

カレンダーもそう、世界のなべあつもそう、あみだくじもそう、プログラムを使えばなんだってできるんです。少し頭を捻って画面の前で指先をカタカタカタッと動かせばだいたいのことはできます。

あなたはそれを自分の手で証明してみせた。ここまでに取り組んでいただいた期間で目覚ましい成長を遂げたと思いませんか。私は思います。

私自身もいろいろと学びがありました。できるだけ初学者の方にわかりやすいように、過度な負荷がかからないように、でも興味やモチベーションを維持しつつ取り組んでいただけるように、その辺の初学者向け書籍に引けを取らないように、工夫をこらしたつもりです。

これまでの連載の反省や日々の経験、私自身の人柄なんかも盛り込みながらどうにかこうにか、ここまで続けてこられました。


次は第二章です。

Webサイトの基礎について学びましょう。その際、HTML, CSS, JavaSriptも基本的なことは抑えられるようにします。

それでは、準備期間に入らせていただきます(けっこう長いかも)。

またお会いしましょう。

まずは第一章、お疲れさまでした & ありがとうございました。

この記事が気に入ったらサポートをしてみませんか?