個サ作 #17 カレンダーLv.7 前編
こんにちは。
前回はカレンダーLv.6をクリアしました。二重ループをやったのでしたね。
今回はカレンダーLv.7として三重ループをします。が、ただ三重ループにするだけなら前回の処理とあまり変わらず、大した労もなく終わってしまいます。
ですので、よりきれいなソースを書いたり実用に向いた機能にしたりするためのテクニックも併せてお伝えしていきます。
今回と次回の2回でやります。後に控えるLv.7.5はおまけみたいなものですので、実質的には今回を最後の取り組みと捉えていただいてもよいです。
では、参りましょう。
カレンダーLv.7 前編
事前準備
事前準備です。今回も新しいモジュールを用意します。モジュール名は「Study6」としてください。以下の動画を参考にどうぞ。
さて、今回もモジュールレベル変数を使うのですが、まずは側を書きます。
Sub R_カレンダー7()
End Sub
はい、OKです。
今回はちゃんとボタンも用意しておきましょう。
動画中はボタンのラベルを「年カレンダー出力」にしちゃってますが、「年カレンダー出力実行」に直しました。月のボタンとサイズ、フォント等を合わせると見栄えが綺麗になります。こんな↓感じ。
はい、ありがとうございます。
今回の実装について
冒頭で
と、言いました。これについてどんな指針で実装していくのかをご説明します。以下の3つです。
変数ひとつの役割を限定的にする
パラメータによって出力内容の変更を可能とする
コーディングのアンチパターンをやめる
1つ目の変数の役割を限定する、という話は実はカレンダーLv.2をやった#5の回の補足でも説明しています。Lv.2では初めてループ処理を扱ったのですが、変数 i にいろんな役割を持たせるのはやめようよ!という話でした。
カレンダーLv.6では変数 i がカウンターと出力行の両方を担っていましたが、今回は純粋にカウンタ変数としての振る舞いにします(※今回は三重ループのため、Lv.6の変数 i の役割は変数 j が担います(後述))。
2つ目の「パラメータによって出力内容を変更できる」ですが、前回は2列目を起点としてカレンダーを出力しましたよね。それを設定次第で柔軟に変更できるようにします。まだあまりイメージできなくて大丈夫。
3つ目のコーディングのアンチパターンをやめる、ですが、これはハードコーディングやマジックナンバーと呼ばれるよくない実装パターンをやめて正攻法の実装をします。詳しくは後述します。
モジュールレベル変数の宣言
では実装に入っていきましょう。まずはモジュールレベル変数の宣言。
実装の進度に合わせて小出しに案内してもいいのですが、説明が冗長になってしまうので、すべて一気に出しちゃいます。
次のソースコードを関数の前に書いてください。
'1列目からカレンダー開始列までの差分
Private Const BASE_OFFSET_COL = 1
'カレンダーの開始行
Private Const START_ROW = 4
'カレンダーの開始月
Private Const START_MONTH = 1
'出力する月数
Private Const WRITE_MONTHS = 12
'エラーメッセージ
Private Const ERROR_MESSAGE = "年の値が不正です。"
'完了メッセージ
Private Const COMPLETION_MESSAGE = "出力が完了しました。"
'カレンダーの開始列
Private Const START_COL = BASE_OFFSET_COL + 1
'日曜日を示す列
Private Const SUNDAY_COL = BASE_OFFSET_COL + 1
'土曜日を示す列
Private Const SATURDAY_COL = BASE_OFFSET_COL + 7
ありがとうございます。すべて定数ですね。定数ということは処理中のソースコードで値を変えることはありません。
詳しい説明は実際に使用する段になってからにします。
この項はこれでOKです。
初期処理関数の実装
それでは本格的な実装に入っていきましょう。
下記のソースコードをマクロR_カレンダー7の後ろに追記してください。
'年間カレンダー出力用初期処理
Function calendarInit()
calendarInit = True
year = getRegexpFirstHit(Cells(2, 2).Value, "^\d{4}(?=年)")
month = START_MONTH
day = 1
If year = 0 Then
MsgBox "年の値が不正です。"
calendarInit = False
Exit Function
End If
'セル状態初期化
Range("B3:S200").Clear
End Function
はい、ありがとうございます。前回やったカレンダーLv.6での初期処理と見比べてみてください。以下の違いがあります。
月情報を取得していない
月情報をエラーチェックにかけていない
月(変数month)の設定値を定数 START_MONTH にしている
戻り値を設定している
変数startColumnを設定していない
まず、月情報についてはお分かりですね。年単位で出力しようとしているので、情報として不要です。12か月分を出力する前提です。なので、取得もしないし当然エラーチェックもしません。
ただ、今回は出力する月の期間をオプションで設定できるようにしています。それがこの1行で
month = START_MONTH
定数START_MONTH自体は先ほど
'カレンダーの開始月
Private Const START_MONTH = 1
このようにモジュールレベル定数として設定しました。
次に戻り値です。カレンダーLv.6ではモジュールレベル変数として宣言した変数startColumnを呼出し元でもチェック結果(処理継続可能か)として利用したので、初期処理関数から値を返す必要がありませんでした。
しかし、今回はそもそも変数startColumnを初期処理では設定しません。ループの中で1から12月の開始日を設定するので、初期処理ではやらない設計としました。
その結果、別の手段で初期処理が正常にできたのかどうかを呼び出し元に教えてあげる必要が出てきました。その対応として今回は戻り値を返すようにしています。
calendarInit = True
calendarInit関数冒頭のこれ↑でまず初期値はTrueとし、年の値チェックに引っ掛かった場合のみ、Falseを返します。
あとは・・・
'セル状態初期化
Range("B3:S200").Clear
↑クリアする範囲がLv.6より増えてます(J列からS列に変わった)が、ここは好きに設定してください。ちょっといろいろ動作確認するうちにそれくらいやっておいた方が無難だな、と思っただけですので。
それでは呼び出し側の処理も書いていきましょう。
ループ前後の実装
先ほど初期処理関数を書いたので、まずはそれを呼び出しましょう。下記のソースコードを下図のように記載ください。
If Not calendarInit Then
Exit Sub
End If
はい、ありがとうございます。カレンダーLv.6では
Call calendarInit
このように書きましたが、戻り値をそのまま利用する場合はCallを書かなくていいというルールがあるのでしたね。あみだくじの#13でもお伝えしました。
ですので
If Not calendarInit Then
と書いたcalendarInitの部分は実質「True」か「False」のどちらかをもっています。
関数calendarInitの戻り値がFalseだったら処理を終了したいので、条件式の前にNotをつけます。これでOK。
せっかくなので今の状態で動作を確認してみましょうか。
これは正常動作をしていますね。クリア処理が働き、その後の処理が未実装ですので、クリアされるだけ、が現在の期待値です。今はこれでOK。
ちなみに、上図の確認で「2月」を削除していますが、ここは残っていても正常動作します。カレンダーLv.5の#11, #12で作った・・・
year = getRegexpFirstHit(Cells(2, 2).Value, "^\d{4}(?=年)")
この正規表現は先頭から始まる文字で後ろに「年」がつく部分を抽出する、というものなので、その後のことは関知していないんです。だから月情報があっても特に問題はありません。
さて、次にいきます。
ループに入る前に必要な変数を定義しましょう。下記を追記ください。
'書き出し行
Dim writeRow As Integer: writeRow = START_ROW
'書き出し列
Dim writeColumn As Integer
'月の最後の日
Dim lastDay As Integer
'最初の日の曜日
Dim firstDayOfWeek As Integer
見たところ、特に変わり者の変数はいないでしょう。おおよそ利用用途は想像がつくと思います。ですので、説明は利用時に譲ります。
さて、もう一息です。次のソースコードはループになるのですが、その実装は後続の項で行うので、ここではループが終わった後の処理を先に組んでおきましょう。
次のソースを下図のように実装ください。
Cells(START_ROW - 1, START_COL).Select
MsgBox COMPLETION_MESSAGE
さて、ちょっとややこしく見えますが、やってることはいたってシンプル、これまでにも出てきたものです。
まずはこちら。
Cells(START_ROW - 1, START_COL).Select
これ、ちょっと想像してほしいんですけど、12か月分出力しきるとするじゃないですか。そうなると処理が終了したとき、セルの選択位置は最後に出力した場所にとどまっているんですよ。
それはちょっと見栄えがよくない!ということで、最初の出力位置に戻す処理を入れています。ここの動きはまた実行時に注目して見てみてください。そのときにまた説明します。
もうひとつ。
MsgBox COMPLETION_MESSAGE
こちらは実装初回の#3から出てきたMsgBox関数に対して定数を渡しています。
当然、出力されるのは定数がもっている値ですね。
このようにね。これもなぜこのようなことをしているかは「今回の補足」の章でお伝えします。定数なんて使わずにMsgBox ~ の場所に直接メッセージを書いておいてもいいのでは・・・?と思っちゃいますよね。
この項についてはこれで終わりです。
ループ一重目の実装①
さて、いよいよループです。
カレンダーLv.7の実装は三重ループと事前に案内しています。
一重目:月
二重目:週
三重目:日
という形でループを組んでいきます。
一重目が月のループということは当然1月から12月をループするのだろうと思うので、そうなると
For i = 1 To 12
Next
こんなループの定義を書きたくなるでしょう。
が、今回の実装指針として以下の案内をしています。
パラメータによって出力内容を変更できる
この場合のパラメータというのは何かしら出力内容に影響を与える設定値です。引数と解釈いただいても構いません。そしてそれはモジュールレベルの定数として用意してあります。
'出力する月数
Private Const WRITE_MONTHS = 12
こんな↑ものがあります。ですのでループは以下のように書きましょう。
For i = 1 To WRITE_MONTHS
Next
これでソース冒頭の設定値次第で好きな月数だけ出力する機能に早変わりです。
今の段階ではソースの中に直接「12」と書くのも定数を書くのも同じじゃないのか?と思った方、おられると思います。
その感覚は正しくて、場数を踏んだりいろんなプログラムに触れたりする中でこのパラメータ化の恩恵がわかります。詳しくは「今回の補足」の章で触れますね。
それではループの中身を見ていきます。
最初の周回で1月分を出力することを想像してみてください。まずどんな情報が必要か、頭の中に少し描いてみましょう。
くどいですが、今回の冒頭で
とお伝えしています。ですので、ここでも無駄な処理をしない綺麗な実装になるような少し工夫をしてみます。
カレンダーLv.6では6週 × 7日で42回、日付の出力処理があったのですが、実際の出力は
If isDayWritable(year, month, day) Then
この処理に寄って取捨選択されるので、どんな月を選んでも28 or 29 or 30 or 31回の出力のみで済む、という動きをしました。間違っても42日まで出力することは決してありません。
今回は月の最終日に達したら残りの不要なループはしない、という処理にしたいと思います。Lv.6では32日目以降はループはするけど出力はしない、という動作でした。今回はそもそもループをしないようにします。
ですので、月のループに入ったらまず最初にその月の最終日情報を取得したい!ということになります。ループを抜ける条件を明確にするためにね。
というわけで一重目のループではまず、月の最終日を取得しましょう。先ほどのローカル変数用意の中でちゃんとその受け皿は登場しています。
'月の最後の日
Dim lastDay As Integer
↑これね。
さて、月の最終日はどうやって取得するんだい?ということになります。もう答えを出しましょう。下記を下図のように追記願います。
'当月の最終日を取得。DateSerial関数の裏技を使う
lastDay = Format(DateSerial(year, month + 1, 0), "d")
ちょっと複雑なことをしていますが、この関数のネスト構造にも慣れてきたでしょうか。最も内側にある関数の戻り値をその上にある関数が引数として利用して、さらに値を返してくれる、という働きをしていますね。
そしてここのコメントにも注目いただきたい。
こう↑書いています。DateSerial関数がなにをしてくれるかというとですね。
年、月、日の各引数に数値を渡すとそれを日付型で返してくれるというものです。
今までString型、Integer型、Boolean型とメジャーなものを扱ってきましたが、Date型というものもあるんです。
で、ですね。まずすみません。コメントには裏技って書いてあるけど、上記の公式リファレンスをよく読んだら裏技でもなんでもなかったです。
こちらをご覧ください。
要するに、日の引数に0を渡すと前の月の最終日を戻り値として返してくれると、書いてあるんです。上記の説明だったらDateSerial(1980, 6, 0)の戻り値は1980/5/31になるということです。今回はこれを利用します。
ですので、
'当月の最終日を取得。DateSerial関数の裏技を使う
lastDay = Format(DateSerial(year, month + 1, 0), "d")
この場合、2024年1月の出力をしようとすると
'当月の最終日を取得。DateSerial関数の裏技を使う
lastDay = Format(DateSerial(2024, 2, 0), "d")
この↑ように引数に値を渡しており、返ってくる値は
'当月の最終日を取得。DateSerial関数の裏技を使う
lastDay = Format(2024/1/31, "d")
となります。
じゃあFormat関数は何をしてくれるんだいと、そういう疑問にあたります。
Format関数は各プログラミング言語に用意されているのですが、日付型を文字列型や数値型に様式変換する、という働きをもちます。
今回の用途としては最終日が欲しいので、この「2024/1/31」から「31」だけを取り出したいんですよね。ではFormat関数の公式リファレンスを見てみましょう。
要所だけ抽出しますと・・・
このような記載があります。「d」を指定すると日付部分を返す、と。
そして、指定内容は
と、あるので、第一引数にフォーマットしたい値を、第二引数に変換したい書式を指定します。今回はあとの引数は省略します。
それを実装したのが、
'当月の最終日を取得。DateSerial関数の裏技を使う
lastDay = Format(DateSerial(year, month + 1, 0), "d")
このソースコードということになります。今回(2024年1月の出力)なら変数lastDayには「31」が代入されます。
ループ一重目の実装②
次にほしい情報はこの月をカレンダー出力していくにあたり、開始列(1日目の曜日の列)はどこか?です。これはカレンダーLv.6でもやりましたね。
次のソースコードを下図のように追記ください。
'初日の曜日を取得
firstDayOfWeek = Weekday(year & "/" & month & "/" & day)
writeColumn = firstDayOfWeek + BASE_OFFSET_COL
ここはカレンダーLv.6でも扱ったのでほぼ省略したいのですが、1点だけ聞いてください。Lv.6ではcalendarInit関数にて
'初期値、初期状態設定
dayOfWeek = Weekday(year & "/" & month & "/" & 1)
startColumn = dayOfWeek + 1
このように変数startColumn に設定するにあたり+ 1をしていました。変数dayOfWeekには曜日を示す値を持っているんだけど、出力は2列目からしてるからその分1を足している、という話でした。
今回は定数BASE_OFFSET_COLを足しています。この定数はというと・・・
'1列目からカレンダー開始列までの差分
Private Const BASE_OFFSET_COL = 1
はい、ソースの最上部で定義していますね。ここの値に応じた列の位置からカレンダーを出力します。これも出力位置を調整するための引数的な役割をするパラメータです。
前回はソースコード中に+1と固定で書いていましたが、この定数が持つ数値を加算することでパラメータに応じた出力位置とすることができます。
次にいきましょう。
下記のソースコードを下図のように追記ください。
'月の印字
Call writeMonth(Cells(writeRow - 1, START_COL))
はい、新しい関数が登場しました・・・が!これは組込関数ではないんです。そしてまだ実装していません。
と、言うわけで実装しましょう。
次の関数をcalendarInit関数の次に書いてください。
'月情報を設定する
Function writeMonth(monthRange As Range)
monthRange.Value = month & "月"
monthRange.Font.Size = 20
monthRange.Font.Bold = True
monthRange.Font.Name = "BIZ UDPゴシック"
Rows(monthRange.Row).RowHeight = 28
End Function
いくつか処理が並んでいますが、上から順に、
値
フォントサイズ
太字か否か
フォントFamily
行幅
を設定しています。値以外は好きにカスタマイズしていただいてOKです。
これで2重目のループが始まる前の処理は完了です。でもまだ一重目のループとしては処理が終わっていません。一通り月の情報を出力したあと、次のループ(月)に行くための準備処理が必要なんですね。
次のソースコードを下図のように追記ください。
'次月処理準備
month = month + 1
day = 1
多めに改行しているところは二重目のループが入ります。これはシンプルに月を表す変数monthを+1し、日を表す変数dayを1に戻しているだけですね。また次の月を1から出力していくためです。
これで終わりかと思いきやもう一声。
ここまで実装していただいた内容の中で次のソースを見てください。
'カレンダーの開始月
Private Const START_MONTH = 1
'出力する月数
Private Const WRITE_MONTHS = 12
モジュールレベル定数です。上記の設定値だったら1月から始めて12か月分出力します。
例えば下半期のみ出力したいとして定数 START_MONTH の値が7だったとしましょう。その時、出力する月数の定数 WRITE_MONTHS が7以上だったらどうなっちゃうのか、ちょっと不安ですね。13月とかないですから。
このケースを回避するためにプログラムの側で期待しない動作を事前に防止するソースコードを書いておきましょう。
次のソースを下図のように追記ください。
'エラー対策
If month > 12 Then
Exit For
End If
モジュールレベル定数の値で存在しない13月以降も処理を継続する設定になっていた場合は強制退場するようにします。変数monthが12を超えたらもうループを抜けるように処理を追加しました。
これで余計な処理やエラーを発生させずに済みます。
今回の実装漏れ回収
すみません。一箇所だけ案内が漏れているところがあります。
If year = 0 Then
MsgBox "年の値が不正です。"
calendarInit = False
Exit Function
End If
calendarInit関数にこのソースあるじゃないですか。
で、こんな↓モジュールレベル定数もあるじゃないですか。
'エラーメッセージ
Private Const ERROR_MESSAGE = "年の値が不正です。"
MsgBox関数に指定している文字列をこの定数に置き換えてください。どうしてこんなことをするのか?は補足の章で触れます。
こんな↑風になればOKです。ありがとうございました。
今回の補足
初期処理結果を使用した条件式にNotを指定している理由
今回実装いただいた初期処理の戻り値ですが、
If Not calendarInit Then
Exit Sub
End If
このように結果を反転させることでExit Subに導くように書いています。関数側で処理続行不可能だったらFalseを返すようにしているから、こういう風にする必要があるんですね。
大概のIf分岐にいえることなのですが、上記の場合だったら下記のように書き換えることも可能です。
If calendarInit Then
'ここにカレンダー出力処理を書いていく
End If
calendarInit関数の戻り値がTrueだったら処理続行なわけなので、それを素直に受け取って(評価して)、Trueだったら入るIf文の中に後続処理を書き連ねてもよいわけです。
なのにそれをやらなかった。
これは私の好みなのですが、いたずらにインデントを下げたくないという想いがあるんですね。#15でお伝えしたWithステートメントを好まない理由に通じているところがあります。
インデントが下がっているということはその分何かしらの分岐や制約を受けているわけですから、そのことを意識しながらソースコードを読む必要があります。
ただ、できるだけ脳の負担を軽くして読みたいので、処理を終了させる条件があればそれはExit Subを明示する形で書いて後続のロジックで読み手に意識させない、という書き方を心がけています。
肌感覚ではやたらにネストが深いソースコードは嫌われる傾向にあると思っています。まあ人それぞれ好みがありますし、特に個人が趣味でプログラミングをする分には自由なので「へ~」くらいに流してもらえたらよいです。
定数でパラメータ化している理由
今回のカレンダーLv.7では処理実行にあたって必要な主要な値はすべて定数で定義しています。完成すれば明確にわかりますが、定数の設定値によって処理結果が異なるような設計としています(出力位置や期間ね)。
これらの定数は引数的な役割をすると本編の中でも言っていましたね。
今までは出力位置も期間も固定値もしくはユーザの入力を受け付ける形でやってきましたが、どうして定数で定義しているのか不思議に思いますよね。
理由は2つあります。
一元管理することで改修の手間や負荷を回避する
ソースコードの保守性・可読性に貢献する
まず一つ目。処理に必要な値を宣言セクションに「一元管理することで改修の手間や負荷を回避する」ことができます。
これ、出力位置や期間を固定値にしていたとするじゃないですか。今モジュール「Study6」のソースコード中の定数から値を取り出している箇所がすべて「1」とか「12」って決め打ちで書いてあると想像してください。
そうなると、上半期だけ出力したい場合や任意の位置から出力したい場合、ソースコードに直接手を入れないといけません。「12」を「6」に書き換えるとか、そういうことです。
これね、今はあまりピンとこないかもしれませんが、一度完成したソースコードにちょっとだけ変更を加えたいというとき、プログラマの心理的にはあまり無暗にソースコードに手を入れたくないんですよ。
それはなぜか。シンプルに間違えたら嫌じゃないですか。だったら初めから「この先、値を変えて利用するかもしれない要素は定数としてソースコードから外に出しておこう」という考えになるんです。
そうしたらその定数値を変えるだけで小さな変更には対応できますからね。
また、定数で一元管理する恩恵として大きいのが、直接コーディングした際の修正漏れ対策です。
ちょっと余談ですが「定数はどんな用途で扱うか?」を説明するときによく用いられるのが消費税です。
生産管理システムや決済システムなんかだと、金額計算時にだいたい消費税が必要ですよね。で、世の中の大きなシステムだとソースコード中に消費税を示す値が何度も出てくることはよくあります。
それもひとつのモジュールに何度もではなく、複数のモジュールに何度もです。そうなると個別に「0.1」とか「0.08」なんて書いてられません。
そこに偶然別の意味合いで「0.1」という値が存在しようものなら改修時に誤って修正するリスクは格段に上がります。
だったら消費税は消費税として明確に定義しておこうじゃないか、という要望に応えてくれるのが定数です。
ここで2つ目のメリット「ソースコードの保守性・可読性に貢献する」の説明に繋がります。
定数を宣言する際、定数名をつけましたよね。当然ソースコード中は値ではなくこの定数が登場します。
MsgBox ERROR_MESSAGE
とか
For i = 1 To WRITE_MONTHS
など。こうして書くことで定数名から処理内容を推し量ることができます。「あぁ、このロジックに入った場合はエラーなんだ」とか「書き出したい月の数分ループするんだ」とかね。可読性向上です。
保守性向上に関しては定数の値を変えるだけで小さな変更には対応できるという点が叶えていますね。
当たり前のように話していますが「保守」という言葉はあまり聞き馴染みがなくイメージしにくいかもしれません。ざっくりですが、一度使いだした機能について不具合を直したり、より便利に改修したりする対応をいいます。
主要な値を定数化しておくことで、ソースコードの再利用をするときなんかも便利なので、このテクニックは覚えておいてください。個人サイトを作成するときにもまた使います。
定数のエラーチェックをしていない理由
今回実装したcalendarInit関数に関して、カレンダーLv.6との違いの一つに
月情報をエラーチェックにかけていない
があります。これ、悩んだ末にエラーチェックしないことにしました。
出力対象の月は
'カレンダーの開始月
Private Const START_MONTH = 1
'出力する月数
Private Const WRITE_MONTHS = 12
この2つの定数値によって決まるわけですが、当然ここにでたらめな値を設定したら予期せぬ動作をするわけじゃないですか。だったらエラーチェックをした方が安全というものです。ではなぜしなかったか。
この定数をいじるのは誰かっていうとまず間違いなくあなたですよね。まあ今回は学習用にやってるからそういう答えが出てくるのですが、そうでないとしてもこの定数値を触るということはコーディングの知識がある人間です。
となると、もう「こっち側の人間」なわけですよ。ITリテラシーもへったくれもないというような素人が触るわけじゃないんです。だったらエラーチェックはええかと、ゆるく考えました。
でも、それでも設定値のミスを見越して・・・
'エラー対策
If month > 12 Then
Exit For
End If
このような処理は今回実装してもらいましたね。一応ケアしています。これは利用時のストレスを減らすためです。
エラーチェックをどの程度までやるか?って結構頭を悩ませるところなのですが、あんまりギチギチにやってもすぐ怒られたりして面倒なんですよ、動作確認のときとかに。
適当な値でちょっと動かしたいだけなのに、とかね。
今回はエラー対策の実装にも大したコスト(労力や時間、行数)がかからなかったためサクッとソースに加えさせてもらいました。
ハードコーディングとは
MsgBox "年の値が不正です。"
この "年の値が不正です。" とか
For i = 1 To 12
この「12」。これ、ハードコーディングです。
ハードコーディングとは定数や別ファイルのようなソースコードの外部で管理した方が適切な情報をソースコード中に直接記載することを言います。
さっき定数化のメリットは保守性の向上にあるといいました。定数化していないということはどんな実装になっているかというと上記のソースみたいにハードコードしてある状態、ということですね。
これのポイントは上記説明中の「ソースコードの外部で管理した方が適切な情報を」という点なんですよ。なんでもかんでも外に出していたら定数だらけになってしまいますからね。
例えばさっき出したソースコードの・・・
For i = 1 To 12
これ。マクロ「R_カレンダー7」のループのところです。「1」の方は定数化せんでええのかと。ハードコードでいいのかと。
いいんです。
今回はこの変数 i は純粋なカウンタ変数です。なにも他の意味を持ち合わせていません。これまでは出力行も担わせたりしていましたけどね、今回は違います。ただのカウンタだからわざわざ外で持つ必要がありません。
だからこれに関しては意図してハードコードしています。これでOK。上記のソース、正しい実装は本編で案内した通り
For i = 1 To WRITE_MONTHS
こう↑ですよね。何か月分出力したいか?は用途によって変わる可能性があるから外(編集しやすい場所)で管理した方が適切だろうということで今回定数にしています。
でね、この話を受けるとね・・・
MsgBox "年の値が不正です。"
こっち↑は別によくない?って思った方もいるかもしれません。
うん。
それはそう。いい。別にいいです。
というかそういう思いが私にもあるからこそ「今回の実装漏れ回収」という項を用意する羽目になってしまいました泣。
じゃあなんで定数にしたんだよ!とヤジが飛んできそうな流れですが、メッセージやラベルは外部管理することが多いです。
なんでもかんでもするわけではないけど、慣例的にだいたいします。
このやり方は個人サイトを作り出してからも出てきます。し、個人サイトの時は慣例的の理由以外にあるその恩恵にお気付きいただけると思います。
ハードコーディングは実装のアンチパターン!これだけ覚えておいてください。
マジックナンバーとは
マジックナンバーという言葉もプログラミングの世界にあります。これ、ハードコーディングととてもよく似ていますが、ソースコード中に出てくる魔法の数字のことをマジックナンバーと言います。
なんのこっちゃ分からないですね。魔法の数字て。
それが何者かは分からないけど、その数字のおかげで処理が正常に動くと。これはまるで魔法の数字だ、と皮肉っているわけです。
教科書的には何を意味するのか分からないソースコード上の数値をマジックナンバーと言います。
先ほどのハードコーディングで挙げた例・・・
For i = 1 To 12
この↑「12」もマジックナンバーです。
が、感覚的にはもっと謎な数値の時にいいますね。これ↑だったら「はいはい、12周すんのね」ってわかりますし。しかもカレンダー処理言うてますから「はいはい12か月ね」ってなりますから。
・・・いや、そうだな。これはマジックナンバーではないですわ。機能名やソースコードの内容からその意味を推定できちゃうから。もっと意味わかんないときに言うんです、マジックナンバーって。
じゃあどんなのがマジックナンバーなんだよ!というと・・・
この↑記事が説明で使ってるのはこれぞって感じのマジックナンバーでした。よかったら見てみてください。都道府県コードを直接数値で扱ってるソースを例示してくれています。
いろいろ言いましたが、定数化は値に名前を付けてその意味合いを明確にしたり、一元管理することで変更漏れを阻止したりする効果があり、それは保守性・可読性の向上につながり結果、関わるみんなが幸せになれる素敵なやつです。
おわりに
おわりです。
いよいよ大詰めですね。あとはループの二重目、三重目を実装すればクリアになります。と、Lv.7.5の実装。
プログラミング言語VBAを使って、プログラミングの基礎を学ぶ過程も次回で最後になります。
後続には第2章、第3章があるのでまだまだ先は長いです。
ただ、ボリューム感としてはこの基礎編が最も多かったかもしれません。基礎、重要ですからね。
適度に休みつつ、でもモチベーションが低下することはないようにコントロールしていただけたら幸いです。
では!甘いもの食べて休もう!他の趣味に講じるもヨシ!美味しいもの食べるもヨシ!寝るもヨシ!運動もヨシ!読書もヨシ!なんでもヨシ!もちろんこのまま次の#18に向かうもヨシです!
今回もありがとうございました。