見出し画像

個サ作 #10 カレンダーLv.4 後編

こんにちは。

前回はカレンダーLv.4の実装を途中まで進めました。はじめて関数を自作したのでしたね。

今回はカレンダーLv.4の核ともいえるisDayWritable関数の実装の続きから始めます。

それでは参りましょう。今回もよろしくお願いします。


今回のゴール

カレンダーLv.4の完成です。

28, 29, 30, 31日それぞれが最終日となるパターンの出力ができるようになります。


カレンダーLv.4 後編

まずはおさらいです。現在、カレンダーLv.4の関連ソース全量は以下のような状態です。

'自作関数の学習
Sub G_カレンダー4()
    'エラーチェック
    If Not isYear(Cells(1, 4).Value) Then
        MsgBox "年の値が不正です。" & vbCrLf & "D1セルに年を示す数字を4桁で入力ください。"
        Cells(1, 4).Select
        Exit Sub
    End If

    If Not isMonth(Cells(1, 5).Value) Then
        MsgBox "月の値が不正です。" & vbCrLf & "E1セルに月を示す数字を入力ください。"
        Cells(1, 5).Select
        Exit Sub
    End If
    
    '初期処理
    Range("E2:E32").Clear
    
    Dim year As Integer: year = Cells(1, 4).Value
    Dim month As Integer: month = Cells(1, 5).Value
    Dim day As Integer: day = 1
    
    '本処理
    Dim i As Integer
    For i = 2 To 32
        If isDayWritable(year, month, day) Then

        Else
            Exit For
        End If
        day = day + 1
    Next
    
End Sub

'年の妥当性チェック
Function isYear(year As String) As Boolean
    isYear = (IsNumeric(year)) And (Len(year) = 4)
End Function

'月の妥当性チェック
Function isMonth(month As String)
    If IsNumeric(month) Then
        isMonth = (1 <= month And month <= 12)
    End If
End Function

'閏年かどうかを返す
Function isLeapYear(year As Integer)
    isLeapYear = year Mod 4 = 0 And Not (year Mod 100 = 0 And year Mod 400 > 0)
End Function

'年月日の妥当性チェック
Function isDayWritable(year As Integer, month As Integer, day As Integer)

    If day <= 28 Then
        isDayWritable = True
        Exit Function
    End If
    
End Function

上記は見やすさ配慮のため、関数の間に1行空行を挟んでいます。

では、続きの実装を始めます。


isDayWritable関数の残件

今、実装してある

    If day <= 28 Then
        isDayWritable = True
        Exit Function
    End If

このソースで変数dayが28までのケースは網羅している、つまりもう値が渡ってくる各種パターンのうち90%はできているんだ、という話をしたのでしたね。

ここからは変数dayが29のとき、30のとき、31のときについて実装していきます。


変数dayが29の場合

29日を出力するかどうか?を気にしないといけないのはまず変数monthが2、すなわち2月の時だけです。そしてその判断材料がうるう年かどうか、ですね。

うるう年なら出力する、うるう年でないなら出力しない、です。

  • 2月以外なら出力可能

  • 2月でかつうるう年なら出力可能

  • 2月でかつうるう年でないなら出力不可

この3点を条件式で表現します。

    If day = 29 And month = 2 And Not isLeapYear(year) Then
        isDayWritable = False
        Exit Function
    ElseIf day = 29 Then
        isDayWritable = True
        Exit Function
    End If
赤枠のソースコードを反映してね

はい、このようになります(いくつか表現方法がありますが、今回はこれで行きます)。

まず最初の条件式

If day = 29 And month = 2 And Not isLeapYear(year) Then

出力していいパターンとしてはいけないパターンに分岐させたいわけですが、まず出力させない方から書いています。

①29日であり②2月である、この2条件が固まれば後はうるう年かどうかのみです。それが

And Not isLeapYear(year)

ここです。2月が28日でおわるときにここの条件式をTrueにしたいので、うるう年ではないのかどうか?を評価しています。Trueだったらうるう年ではない、ですね(Not演算子によって反転されるから)。

その結果、Ifの中では

        isDayWritable = False
        Exit Function

戻り値を示すisDayWritableにFalseを代入しています。Falseは出力不可を意味します。そしてExit Functionで関数の処理を終了ですね。


ではもう一方の29日が出力可能の方も見てみましょう。

    ElseIf day = 29 Then

はい、Elseを使いたいのですが、ElseIfとして再度変数dayが29であることを条件にしています。

これを指定しないと30や31の時もここに入っちゃうんですよね。だからあくまで29日に限定した条件式だぞ、ということでこのようにしています。

Ifの中は先ほどの逆で

        isDayWritable = True
        Exit Function

isDayWritableにTrueを代入し、呼び出し元に返しています。そして関数脱出!(Exit Function)ですね。


この変数dayが29のロジックはこれで完了です。が!ひとつ補足させてください。実はここ、ロジックとしては・・・

    If day = 29 Then
        'うるう年ではない2月以外、29日は出力可能と判定する
        isDayWritable = Not (month = 2 And Not isLeapYear(year))
        Exit Function
    End If

こう書いても同じ動きをします。こちらの方がスマートですね。ポイントはコメントにあります。

        isDayWritable = Not (month = 2 And Not isLeapYear(year))

この1行だけでは意図がわかりづらいでしょう。特にNotで囲んだ中でもう一度Notを使ってしまっています。これは正直見ただけで辟易してしまうソースです。アンチパターンと言ってもいい。

こういうときにコメントが力を発揮するんですね。そこに

        'うるう年ではない2月以外、29日は出力可能と判定する
        isDayWritable = Not (month = 2 And Not isLeapYear(year))

このようにコメントを付与することで、ややこしいソースだが何をしているのかが即座に理解できます。

ソースだけだとちょっと読みにくいとは思いますが、And演算子を使っている場合、一つでもFalseの条件があればその式はもうFalseになります(全部TrueのときだけTrueを返す演算子ですので)。

だから今回の場合、変数monthが2以外だったらその時点でFalseを返し、外側のNot演算子により反転されるから結果的にTrueを返す、すなわちその29日は出力可能、ということになります。

そうなると

Not isLeapYear(year)

こちらの条件を考えないといけないのは変数monthが2のときだけ、ということになります。あとはこれがNot演算子も含めて読み解くと

  • うるう年ならFalseになる

  • うるう年でないならTrueになる

となるので、

  • 2月でうるう年ならFalseとなり外側のNot演算子によりTrueへ(出力可能)

  • 2月でうるう年ないならTrueとなり外側のNot演算子によりFalseへ(出力不可)

と、なります。

どちらのソースを採用いただいてもOKです。


呼び出し元の実装

今の段階で2月に関してはisDayWritable関数の実装が完了したことになります(最大でも29日までですのでね)。ですので、ここで呼び出し元の実装を完成させてしまいましょう。

現在、ループが

    For i = 2 To 32
        If isDayWritable(year, month, day) Then

        Else
            Exit For
        End If
        day = day + 1
    Next

このようになっているのでした。isDayWritable関数の戻り値がFalseだった場合は、Elseに入り、そのままExit For つまりループを抜けるという実装になっています。

これは前回もお伝えしました通り、一度Falseが返ってきたらもうそれ以上出力する必要がないことを意味するからですね。下記引用します。

Falseが返ってくるのはこれ以上日付の出力をしなくていいことを意味します。ですから、これ以上の周回は不要とし、Exit Forをしています。

個サ作 #9 カレンダーLv.4 前半

ではTrueが返ってきた場合、すなわち出力を継続したい場合の処理を書きましょう。以下のようになります。

            Cells(i, 5).Value = day & "日"
            Application.Wait [Now() + "0:00:00.05"]
            Cells(i, 5).Select
赤枠のソースコードを反映してね

はい、特に新しい要素がないんです。

変数dayの値を"日"と連結した形で出力、処理が一瞬で終わるから0.05秒だけ待機、選択セルを出力したセルに合わせる、の3処理をしています。

試しに2024年2月と2025年2月で実行してみましょう。

2024年2月、2025年2月で実行する様子

はい、2024年2月は29日まで、2025年2月は28日までの出力になりましたね。OKです。次行きましょう。


変数dayが30の場合

変数dayが30の場合です。

ここのロジックは単純であなたもご存じ、30日は2月以外のすべての月にある!んですよね。

ですので、これをそのまま条件式に落とし込めばいいだけです。

    If day = 30 And month <> 2 Then
        isDayWritable = True
        Exit Function
    End If
赤枠のソースコードを反映してね

こうです。はい、シンプルですね。

別の表現方法として

    If day = 30 Then
        isDayWritable = month <> 2
        Exit Function
    End If

これでも同じ動作をします。私の好みではこっちかな~。

さっきの29日の条件式でもそうですが、やはり「If」の横にゴテゴテと式が書いてあると読むときに少し身構えてしまうんですよね。

それに

    If day <= 28 Then
    If day = 29 Then

と来ていたらやはり

    If day = 30 Then

こちらの方が美しいです。でもどちらでもOKです!わかりやすい方!


変数dayが31の場合 前半

さていよいよ大詰めなのですが、ここで新要素の登場です。

31日の出力可否を判定するときに気にしたいのが、変数monthの値はなんなのだい?というところですね。

31日を出力できるのは1, 3, 5, 7, 8, 10, 12月だけです。となると変数monthの値を検証せねばなりません。

ここまで積み重ねたものがあるあなた。

If day = 31 And (month = 1 Or month = 3 Or month = 5 Or month = 7 Or month = 8 Or month = 10 Or month = 12) Then

こんな式を思い浮かべたでしょう。はい、これもひとつの正解です。似たようなものを#7のカレンダーLv.3で実装しました。


ところがどっこい!そうは問屋が卸させない!

これをもう少しテクニカルな方法で実装したいと思います。


配列を使ってみよう

#4で「配列」という言葉が出てきています。

これまでは中身が変わりゆく値を保持するときに変数を使用してきました。変数は値を常にひとつだけもつことができますね。ループ処理で使っているday変数で言うと、1があるところに2を代入すると1は消えます。

データを保持する形式に「配列」というものがあります。これは複数の値をセットで扱えるものと捉えてください。下図のイメージです。

上は変数、下は配列のイメージ

通常の変数が「a」という値だけを保持するものだとすれば、配列は1番目には「a」、2番目には「b」、3番目には「c」、4番目には「d」という値を保持できる、というものです。

でね、今回変数monthの値が1, 3, 5, 7, 8, 10, 12のいずれかに該当するかどうか?のチェックは

If day = 31 And (month = 1 Or month = 3 Or month = 5 Or month = 7 Or month = 8 Or month = 10 Or month = 12) Then

この条件式みたいにひとつずつチェックするのではなく、配列内の要素に含まれるかどうか?という方法で検証します。

となると、まずやりたいのは配列の宣言および定義ですね。

配列の用意の仕方として変数を宣言するようなスタンダードなやり方もあるにはあるのですが、それは#14(あみだくじ中編)でやるので、今回はよりお手軽な方法を使います。

まず、次のソースをisDayWritable関数の最下部に記載ください。

    '31日まである月を格納する配列
    Dim monthWith31DaysArray() As String
    monthWith31DaysArray = Split("1,3,5,7,8,10,12", ",")
赤枠のソースコードを反映してね

さて、ソース解説です。まずは変数宣言部。

    '31日まである月を格納する配列
    Dim monthWith31DaysArray() As String

こちらはもう見慣れたものですね。ただし、一箇所だけ見たことないものがあるでしょう。変数名の末尾に括弧がついています。VBAにおいてはこれが配列であることの証になります。

データ型がStringのため、配列の各要素が文字列型となります。これ、数値を格納するのにStringを指定したのはこの後の説明が関連しています。

では次の行を見てみましょう。

    monthWith31DaysArray = Split("1,3,5,7,8,10,12", ",")

はい、Splitスプリット関数の初出です。

この関数が何をしてるかと申しますと、最初の引数に指定した文字列を2つ目の引数に指定した文字で区切った各要素を配列型のデータとして返す、という働きをします。

    monthWith31DaysArray = Split("1,3,5,7,8,10,12", ",")

見て。第一引数は数字をカンマ区切りで、第二引数はカンマを指定しています。

つまり、下図のようなイメージです。

今回のSplit関数がやってくれることのイメージ

デバッグをしてローカルウィンドウからも値の様子を見ていただきましょう。

デバッグをしてローカルウィンドウから配列の中身を確認する様子

ローカルウィンドウを見てみましょう。

変数monthWith31DaysArrayのデータ型がString(0 to 6)となっており、各要素に値が入っているのがわかるでしょうか。

これ、Split関数からの戻り値である配列をInteger型配列で受け取ろうとするとうまくいかなかったんですよね。Split関数の戻り値はString型の配列なためです。だから変数monthWith31DaysArray はString型としています。

指定された数のサブ文字列が含まれる 0 ベースの 1 次元配列を返します。

Split 関数

ひとつ注意点がありまして、通常数字を数えるときって1からだと思うんですよ。でも配列の要素は0番目から始まります。

配列の添え字は0から始まる

この何番目の要素か?を示す数字は添え字とかインデックスと言います。


さて、ここまでで31日まである月を示す数字を格納した配列の用意はできました。次はこの配列を使って、指定した値が配列に含まれるのかどうか?を判定してくれる関数を作りましょう。

(先ほどの実行は中断し、ブレークポイントも外します)


isExistInArray関数

カレンダーLv.4における最後の自作関数、isExistInArray関数を作ります。

まずは名称の解説からしましょう。下記を下から上に読んでみてください。

  • is・・・かどうか

  • Exist・・・存在する

  • In・・・の中に

  • Array・・・配列

これらの英単語を連結してisExistInArray関数ですね。

この関数は2つの引数が必要です。

  • 配列中にあるかどうかを調べる値

  • 調べる対象の配列

の2つです。というわけで、側は下記のようにしましょう。今回は学習がてら戻り値の型も書きましょう(引数情報の右にAs [戻り値のデータ型]という書き方ができます。)。

'指定した値が配列に含まれるかどうかを返す
Function isExistInArray(checkValue As Integer, targetArray() As String) As Boolean

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

配列中のいずれかの要素に指定した値があればTrueを、なければFalseを返します。

この関数では第二引数の配列中に第一引数の値があるかどうかを検証するのですが、関数を作る時はだいたい例外ケースも考えないといけないんです。例外ケースとは基本的には想定していないケースですね。

この関数で言うと以下の2つが挙げられるでしょう。

  1. 引数checkValueチェックバリューが空だった時場合

  2. 引数targetArrayターゲットアレイに要素がなかった場合

ひとつ目に関しては今回は無視します。というか、あえて配列中に空文字の要素があるかどうかを探したい、というケースもそれはそれでOK、という方針で行きます。

ではふたつ目の引数targetArrayに要素がなかった場合について。こちらはきちんと対処したいと思います。探される側の配列がないということは処理が成立しないことを意味しますからね。

次のソースをisExistInArray関数の中に書いてください。

    If UBound(targetArray) = -1 Then
        'UBoundの戻り値:-1は要素数0を示す。含まれていない、とする
        isExistInArray = False
        Exit Function
    End If
上図のように実装してね

はい。ここでまたしても新しい関数の登場です。

UBound関数です。これは引数に渡した配列の要素数を返してくれます。

↑こっちの方がわかりやすいかな。

厳密には配列の最も大きい添え字/インデックスを返してくれるので、今回「1,3,5,7,8,10,12」は要素数で言うなら7ですが、0から始まる添え字のため最大のインデックスは6になります。

つまり、返してくる数値は6。要素数が1つだけならそれは0番目のインデックスなので、0を返してきます。

じゃあ要素がないときはどう返してくるの?というのが今回の問題ですね。

はい、こちらの赤下線にご注目です。-1を返してきます。ですので

    If UBound(targetArray) = -1 Then
        'UBoundの戻り値:-1は要素数0を示す。含まれていない、とする
        isExistInArray = false
        Exit Function
    End If

このロジックによって、要素がなにもない配列であればcall側にFalseを返す処理の出来上がりです。関数名にFalseを代入したらあとは処理を終了するためのExit Functionですね。

これができたら後は心置きなく配列中に指定した値が存在するかどうかを探す処理に専念できます。


ここからは意外にまどろっこしいことをします・・・というとだいたい想像つきますかね。配列の各要素を一つずつ確認していきます。

次のソースコードを追記してください。この実装でisExistInArray関数としては終わりです。

    Dim i As Integer
    For i = LBound(targetArray) To UBound(targetArray)
        If CInt(targetArray(i)) = checkValue Then
            isExistInArray = True
            Exit For
        End If
    Next
赤枠のソースコードを反映してね

ループ処理をしていますね。これまで行ってきたループ処理は

    For i = 2 To 32
        
    Next

このように開始値と終了値が固定のパターンが多かったのですが、今回は可変値かへんちです。

開始/終了値を変数に持っていたり、なにかの処理によって導出したりするようなパターンを動的な処理などと言います。

状況に応じて値がくんですね。固定ではない、と解釈ください。

では、まずこちらから解説しましょう。

    For i = LBound(targetArray) To UBound(targetArray)

UBound関数については先ほども出てきました。最大の添え字を返してくれるのでしたね。これを終了値の方で使っているということはもうひとつのLBound関数の方も察しがつくでしょう。

ここまでの説明で配列の添え字は0から始まる、という話をしました。ですので、その話に従うと

LBound関数なんて使うまでもなく0で固定なのでは?

と思うかもしれません。はい、その感覚は正しいです。ただですね、実装によっては最初の添え字が0ではないこともあるんです。これは#16で登場します。

ですから、どちらの場合でも対応できるように最小のインデックスも動的に取得するように書いています。

(「インデックス」と「添え字」は同じ意味です)

今回の場合、変数targetArrayの中身は1, 3, 5, 7, 8, 10, 12なので、

    For i = LBound(targetArray) To UBound(targetArray)

    For i = 0 To 6

ということになります。

あとはループの中で行っているこの処理ですね。

        If CInt(targetArray(i)) = checkValue Then
            isExistInArray = True
            Exit For
        End If

解説します。1行目でお伝えしたい点は2つです。

まず、配列要素へのアクセスから参りましょう。先ほども申しましたが、配列を最初の要素から最後の要素までループしてるということは、要するにひとつずつ検査対象の値と一致するかを確認していきます。

その時に必要になるのが、配列内の要素にアクセスすることです。それをしているのが・・・

targetArray(i)

これですね。説明のために変数targetArrayの中身をローカルウィンドウに表示したいので、次のソースを下図のように追加してください。

Call isExistInArray(month, monthWith31DaysArray)
赤枠のソースコードを反映してね

下図のGif画像でデバッグをしてローカルウィンドウへ表示しています。

デバッグの様子

さて、ローカルウィンドウです。

ローカルウィンドウに表示された配列の中身

ループの1周目の段階で変数 i が0で、この値は6までインクリメントします。

そしてtargetArray(0)なら値は"1"、targetArray(1)なら値は"3"、targetArray(2)なら値は"5"・・・という具合に配列の各インデックスに値が格納されていますね。

targetArray(i)

この書き方をすると変数 i の値が配列の添え字にあたるので、次から次へと周回が進むたびに次の要素を走査する、という動きができます。

If CInt(targetArray(i)) = checkValue Then

では、この条件式のうちtargetArray(i)の部分は各要素がもつ値になることは分かりました。そしてイコールを使用し右辺と値を比較していますね。

となると残るはCInt関数です。

このisExistInArray関数の宣言部を思い出してほしいのですが、

'指定した値が配列に含まれるかどうかを返す
Function isExistInArray(checkValue As Integer, targetArray() As String) As Boolean

2つの引数のデータ型を見てください。Integer型とString型です。

この処理ではInteger型の変数checkValueとString型を保持する配列の中身を比較します。はい、ここで型違いが起きていますね。


実を言いますと、ここでも#4で学習した暗黙的型変換が起きるので、CIntシーイント関数を用いなくても問題なく処理は回ります。

が、より正確性を期すために今回はあえて明示的型変換をしてみましょう、という試みですね。

先ほども確認したローカルウィンドウです。右端の「型」の欄をご覧ください。配列内の各値はStringとなっていますね。

これをCInt関数で包んであげることで、数値型の値として使用することができます。今回はCIntの戻り値を比較にそのまま利用していますが、より分かりやすさを重視すると・・・

    Dim i As Integer
    '数値型の比較用変数
    Dim compareValue As Integer 
    For i = LBound(targetArray) To UBound(targetArray)
        'ここで、配列の値を数値型に変換したものを受け取る
        compareValue = CInt(targetArray(i))
        'そして比較に使用する
        If compareValue = checkValue Then
            isExistInArray = True
            Exit For
        End If
    Next

このようになります。ソースコードは元のままでもこちらを採用してもどちらでもOKです。

参考としてInteger型のcheckValueとInteger型に変換されたcompareValueをローカルウィンドウで確認する様子です。どうぞ。

変数dayが31の場合のロジック内から呼び出される関数の実装が完了しました。次項で呼び出し元の続きを実装しましょう。


変数dayが31の場合 後半

それでは最後にisExistInArray関数を呼び出す部分を実装し、完了へもっていきましょう。

前項で変数確認のために書いた

Call isExistInArray(month, monthWith31DaysArray)

これは削除してください。

そして、以下を実装しましょう。

    If day = 31 Then
        isDayWritable = isExistInArray(month, monthWith31DaysArray)
        Exit Function
    End If
赤枠のソースコードを反映してね

はい、これで変数dayが31のとき、変数monthの値が1, 3, 5, 7, 8, 10, 12のときは呼び出し元にTrueを返し、そうではないときは30日までの月なのでFalseを返す、という動作をするようになりました。

もともと「G_カレンダー4」マクロからの呼び出しでisDayWritable関数の処理をしていましたが、ここからさらにisExistInArray関数を呼び出す、という処理の流れになっています。

関数から関数を呼び出すことは全然よくあるスタンダードな手法で、これが多くなるとまるでたらい回しにでもされているような錯覚を覚えますが、処理の単位を適切に分けた結果、そうなるときはそうなります。


最後に動作確認

それでは、今回で28日まで、29日まで、30日まで、31日までの各パターンに対応できる処理が組めたことですし、最後にそれぞれの動作確認をして終わりにしましょう。

まずは2027年2月(28日)と2028年2月(29日)。

次に、2028年4月(30日)と2028年3月(31日)をやります。

はい、期待値通りの挙動をしますね。OKです。


今回のふりかえり

今回は以下の要素を学びました。

  • 配列

変数としてはひとつだけど、複数の値を管理できること、その値へのアクセスには添え字(インデックスともいう)を使うこと、主には添え字は0から始まることを抑えておいてもらえたらOKです。


今回の補足

Exit Functionを用い、Elseを使用しなかった理由(蛇足)

このカレンダーLv.4で作ったisDayWritable関数について、一部補足させてください(蛇足のような内容です)。

プログラムって状況に応じて異なる書き方をしても同じ結果を返すことはよくあります。では、どうして異なる書き方があるかというと、それはコーダー(プログラマ)の好みやパフォーマンスによるものです。

今回のisDayWritable関数は各If文の中でExit Functionをしましたよね。

Exit Functionに赤下線を引いた図

でもこの処理って次のように書いても同じ動きをします。

ElseIfに赤下線を引いた図

いかがでしょう。Exit Functionを排して各If文をElseIfで繋ぎました。

一応ソース置いておきます。

    If day <= 28 Then
        isDayWritable = True
        
    ElseIf day = 29 Then
        'うるう年ではない2月以外、29日は出力可能と判定する
        isDayWritable = Not (month = 2 And Not isLeapYear(year))
        
    ElseIf day = 30 Then
        isDayWritable = month <> 2
        
    ElseIf day = 31 Then
        '31日まである月を格納する配列
        Dim monthWith31DaysArray() As String
        monthWith31DaysArray = Split("1,3,5,7,8,10,12", ",")
        isDayWritable = isExistInArray(month, monthWith31DaysArray)
        
    End If

Exit Functionを使用した場合はいずれかの条件式でTrueとなれば他のIf文には入らない、という流れになるので、いずれか一つの評価のみに入るElseIfを用いた場合も同じ動作となります。

個人的には短くて簡素なソースコードを好むため、後者のElseIfを使用したロジックの方がしっくりはきます。

ただですね、ソースコードを読んで処理内容を理解するときの流れがExit Functionを使う場合よりも冗長なんです。

If文をそれぞれ独立させてTrueで中に入ればExit Functionで逃がす場合、下図のように独立したロジックの連続だからひとつひとつを理解しやすいです。

処理の塊を赤枠線で示した図

つまり、ソースコードをは冗長でも解釈・理解の過程は簡素なんですね。

これも処理の塊を赤枠線で示した図。さっきと比べて赤枠の数は減ったけどひとつが大きい

こっちだとIfからEnd Ifまでが長いからday <= 28の条件式を読んだ後も頭が自由になりません。「・・・じゃなかったら(ElseIf)」を3回繰り返して全体の流れを理解しないといけません。

ひとつのIf条件式を読み終えてもまだIfのセンテンスの中にいるという読解上のストレスがついて回るんですね。

(あとで読み返すと、これは完全に好みの問題ですわ。)

ちょっと細かいことですが、こういうポイントに注意を払いながら実装ができるようになるとあなたにとっても読みやすいソースコードが書けるようになります。

初心者のうちはあとで読み返したとき、自分で書いたソースコードなのに何をやってるのかわからない!ということがありがちですからね。

長々と話しましたが、同じ動きをするので、どちらでもOKです。この先も「この人はこう説明してるけど私としてはこっちのロジックの方がいいな」と思うことがあればドシドシ改編いただいてOKです。

大事なのは自分の頭で考えることです。間違っても全然OK、その過程で経験値を積んでいきます。


関数を作る時の心がけ①

今回作成いただいたisExistInArray関数で、実装内容に一部疑問を抱かれた方がいるのではないか、と思っています。

その実装はどこかというと・・・

If UBound(targetArray) = -1 Then
    'UBoundの戻り値:-1は要素数0を示す。含まれていない、とする
    isExistInArray = False
    Exit Function
End If

ここ!

当たり前のようにここの解説をしましたが、呼び出し元がね・・・

    Dim monthWith31DaysArray() As String
    monthWith31DaysArray = Split("1,3,5,7,8,10,12", ",")
    
    If day = 31 Then
        isDayWritable = isExistInArray(month, monthWith31DaysArray)

こんな風に配列をちゃんと作ってから関数に渡してるからisExistInArray関数中の引数targetArrayに要素がないというケースはあり得ないんです。

つまり、isExistInArray関数の

If UBound(targetArray) = -1 Then

この条件式は必ずFalseになる、と言えます。

にもかかわらず今回要素がないケースを想定した実装にしたのはなぜか。

これは関数を作る時の心構え、というかシステムづくりの定石みたいなものなのですが、「今回動けばいい」ではなく「1関数としてどうあるべきか?」という視点で処理を作るんですね。

第1引数の検証値と第2引数の配列を用いて配列中に検証値に一致する要素があるかどうかを探す内容の関数、となればもしも空っぽの配列が渡されたときはどんな動きになるのか?はきちんと実装しておくのがあるべき姿です。

そうしないと、予期しない動き、すなわちバグの可能性を抱えた機能になってしまうからです。

なので今回、必ず要素の詰まった配列がisExistInArray関数に渡されるんだけど、要素がない配列が渡されたパターンもちゃんとすくうような実装にしました。


とはいえ、これを遵守するのは初心者にとっては気が重いと思います。「今」なくても問題ないロジックを実装するのって面倒ですからね。

ですから、そんなに真剣に守ることはありません。

もっとプログラミングを継続していくとその先で、この話に実感を伴って心から同意いただける日が来ると思いますので、その時を迎えてから実践されるのでも全然遅くはありません。


関数を作る時の心がけ②

補足の補足です。

前項の話に納得がいかない、という方。その感覚、それはそれで正しいです。というのもYAGNIヤグニという言葉がありましてね

プログラミング界隈では「YAGNI原則」とか「YAGNIの法則」として親しまれています。

これはぎゅっとまとめて言うと余計な機能は実装するな!必要になったときでヨシ!ということになります。

ですので、この原則に従うと前項で説明しましたisExistInArray関数の

    If UBound(targetArray) = -1 Then
        'UBoundの戻り値:-1は要素数0を示す。含まれていない、とする
        isExistInArray = False
        Exit Function
    End If

このロジックは不要、ということになります。

ただですね、このYAGNIの法則、結構人によって解釈に幅があったり、適用するかどうかもケースバイケースですので、あまり鵜呑みにするのも危険というものです。

ですので、まあそういう言葉があるんだな、程度にとどめておいてください。


デバッグ時、変数値の簡易確認方法

もう少し早くお伝えしてもよかったのかもしれませんが、デバッグをした際、ローカルウィンドウを見ずとも変数が今格納している値を確認する手段があります。

ツールチップから変数の値を確認している様子

デバッグ中にカーソルを変数の上に合わせると小さなツールチップに変数の今の値が表示されます(上図参照)。

ループに入ってからは[F5]キーを押下しています、その際カーソルが変数 i に乗ったままになっているので、次のブレークポイントまで早回しするたびに変数 i の値もインクリメントしていることが伺えます。

デバッグ時には割と便利なテクニックですので、活用してみてください。


おわりに

終わりです!カレンダーLv.4が完成しました。

カレンダーLv.1 順次処理
カレンダーLv.2 反復処理
カレンダーLv.3 分岐処理
カレンダーLv.4 自作関数

各レベルでいろいろやってきてますが、大雑把にはこんな感じ少しずつできることを増やしていってます。

正直、高揚してほしいです。これはすごいことですよ。今まだカレンダーとなべあつくらいしかしてないからピンと来ないかもしれませんが、これだけ基礎を抑えたらできることが格段に増えていきます。

で、基礎基本の網羅範囲が広がっているということはこの先は内容も少しずつ毛色が変化していきます。基礎を習ったということは?そう、応用です。

世の中にはシステムが数多ありますが、結局どれも中身は基礎の応用でできています。たしかに高度な技やテクニカルでトリッキーなやり口もありますが、結局それらも原点は基礎にありです。


次回は正規表現せいきひょうげんをやります!カレンダーLv.5はソースコード的には少ないですが中身は濃いです。すごい濃厚です。そしたら次はいよいよあみだくじ・・!

今回もありがとうございました。ゆっくりお休みになってください。


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