部屋割りマクロ(Excel)のリファクタリング(3)
部屋割りマクロのリファクタリング
傍流の処理を外に出す
前回の
のリスト2では、大まかに言って
- 引数のチェック
- 定員表の配列化
- 振り番処理
の3つの処理を行っていた。
メインの処理は、当然 3. の振り番処理。あとの2つは、そのための準備という位置づけだ。
今回は、とりあえず行数を食っている 1. を外に出すことを試みた。
引数チェックの過程を外に出す
引数をチェックするFunctionを別途作って、メインのプロシージャからはそのFunctionを呼び出す、という形にする。
もちろん、全ての引数をチェックするFunctionを1つ作ることも可能だが、なるべく
1プロシージャ1機能
を旨とする。まあ、
血盟団
の
一人一殺
みたいなもんだな。←違う
引数チェック用プロシージャの数々
リスト1ー1 標準モジュール
Private Function isAppropriateAsTargetRange( _ ByVal targetRange As Range) As Boolean If targetRange.Columns.Count <> 1 Then isAppropriateAsTargetRange = False Else isAppropriateAsTargetRange = True End If End Function
引数targetRangeが、振り番対象セル範囲として適切かどうかを判定するだけのFunction。振り番対象セル範囲は、列数が「1」でないといけないので、それを判定しているだけ。
他に使い道もないし、わざわざ汎用的にするまでもないレベルの処理なので、Privateにして、振り番マクロからの呼び出され専用にしている。
プロシージャのアクセス修飾子については、基本的に
完全専属子会社みたいなプロシージャはPrivate
というふうにしている。
リスト1ー2 標準モジュール
Private Function isAppropriateAsCapacityTabele( _ ByVal roomData As Range) As Boolean If roomData.Columns.Count <> 2 Then isAppropriateAsCapacityTabele = False Else isAppropriateAsCapacityTabele = True End If End Function
コチラも、引数roomDataが、部屋定員表として適切かどうかを判定しているだけ。最低限部屋定員表は2列構成でないといけない、ということ。
リスト1ー3 標準モジュール
Private Function isAppropriateAsRoomDataTable( _ ByRef roomDataArray As Variant) As Boolean '……(1)' Dim maxIndex As Integer maxIndex = UBound(roomDataArray, 1) Dim tmp As Variant '……(2)' Dim i As Integer For i = 1 To maxIndex tmp = roomDataArray(i, 2) If Not IsNumeric(tmp) Then _ isAppropriateAsRoomDataTable = False: Exit Function '……(3)' If tmp < 1 Then _ isAppropriateAsRoomDataTable = False: Exit Function '……(4)' If tmp - CInt(tmp) <> 0 Then _ isAppropriateAsRoomDataTable = False: Exit Function '……(5)' Next isAppropriateAsRoomDataTable = True '……(6)' End Function
コイツは、今回新たに追加したチェック機能。部屋定員表の2列目のデータ、すなわち部屋の定員数が有効な数値であるかどうかを判定する。
(1)の
Private Function isAppropriateAsRoomDataTable( _ ByRef roomDataArray As Variant) As Boolean
では、引数と返り値を設定。
引数として部屋定員表を格納した2次元配列roomDataArrayを受け取り、チェックを通過したらTrue、途中で引っかかったらFalseを返すようにしている。
受け取る引数が特殊なので、当然Private指定。
(2)の
Dim tmp As Variant
は、配列roomDataArrayの要素を一時的にぶち込んでおくための変数。この後の処理で何度も何度も「roomDataArray(i, 2)」と書くと読みづらくなるのでこうした。
どんな型の値がぶち込まれるか分からないので、Variant型にしている。
(3)~(5)は値のチェック。
(3)の
If Not IsNumeric(tmp) Then _ isAppropriateAsRoomDataTable = False: Exit Function
では、まずtmp、つまりroomDataArray(i, 2)の値が数値として評価できるかどうかを判定。数値でなかったら、すなわち判定結果がFalse(Not True)ならば、Falseを返して処理を抜ける。
(4)の
If tmp < 1 Then _ isAppropriateAsRoomDataTable = False: Exit Function
この段階で、数値であることは確認済みなので、ここで無効な数値でないかチェックする。「0」とか、負の数なんかは、ここで排除される。
(5)の
If tmp - CInt(tmp) <> 0 Then _ isAppropriateAsRoomDataTable = False: Exit Function
では、整数になっているかどうかのチェック。
一度、
If TypeName(tmp) <> "Integer" Then
としてみたが、うまくいかなかった。
たとえば、A1セルに「2」が入っているとして、イミディエイトに
?TypeName(Range("A1").value)
と打ち込んで[Enter]を押したら、
Double
と表示されたんですよねー。
で、仕方なくこんなアホみたいなコードを書いたわけです。
tmpが整数だったらCInt(tmp)の返り値もtmpだろう、ということです。
リスト1ー4 標準モジュール
Private Function isOverCapacity( _ ByRef roomDataArray As Variant, _ targetNumberOfPeople As Integer) As Boolean '……(1)' Dim maxIndex As Integer maxIndex = UBound(roomDataArray, 1) Dim roomCapacity As Integer Dim i As Integer For i = 1 To maxIndex '……(2)' roomCapacity = roomCapacity + roomDataArray(i, 2) If roomCapacity >= targetNumberOfPeople Then _ isOverCapacity = False: Exit Function Next isOverCapacity = True End Function
コイツは、部屋の定員数が足りているかどうかを判定する。
(1)の
Private Function isOverCapacity( _ ByRef roomDataArray As Variant, _ targetNumberOfPeople As Integer) As Boolean
で引数と返り値を設定。
第1引数roomDataArrayはリスト1ー3でも登場した部屋定員表の配列。
第2引数targetNumberOfPeopleは、振り番対象セルの数。
んで、振り番対象セルの数が部屋定員を超えていたらTrue、定員内に収まっていたらFalseを返す。
(2)からの5行(実質4行)
For i = 1 To maxIndex roomCapacity = roomCapacity + roomDataArray(i, 2) If roomCapacity >= targetNumberOfPeople Then _ isOverCapacity = False: Exit Function Next
では、ForループでroomDataArrayの2次元目の要素(要するに部屋の定員数)を足し合わせていき、その数がtargetNumberOfPeople、つまり振り番対象セルの数と同じになるか超えるかすれば、定員オーバーではないということになるので、その時点でFalseを返す、という形にしている。
メインのコードの書き換え
リスト1ー1~4の処理を外出ししたのに伴い、メインのコード(前回記事のリスト2)を書き換える。
リスト2 標準モジュール
Public Function assignOfRooms(ByVal targetRange As Range, _ ByVal roomData As Range, _ Optional ByVal hasHeader As Boolean = True) _ As AssignRoomsResult On Error GoTo errorHandler 'targetRangeが不正ならば終了 ※リスト1ー1' If Not isAppropriateAsTargetRange(targetRange:=targetRange) Then _ assignOfRooms = failedByMultipleColumns: Exit Function '部屋定員表が不正ならば終了 ※リスト1ー2' If Not isAppropriateAsCapacityTabele(roomData:=roomData) Then _ assignOfRooms = failedByNotTwoColumnsTable: Exit Function '部屋定員表の項目ラベルがあるときは、項目ラベルを表から除く' With roomData If hasHeader Then _ Set roomData = .Offset(1, 0).Resize(.Rows.Count - 1, .Columns.Count) End With '部屋定員表を2次元配列化する' Dim roomDataArray As Variant roomDataArray = roomData.Value '部屋定員表が不正な内容ならば終了 ※リスト1ー3' If Not isAppropriateAsRoomDataTable(roomDataArray:=roomDataArray) Then _ assignOfRooms = failedByIrregularRoomDataTable: Exit Function '部屋割り対象人数がキャパオーバーなら終了 ※リスト1ー4' If isOverCapacity(roomDataArray:=roomDataArray, _ targetNumberOfPeople:=targetRange.Count) Then _ assignOfRooms = failedByOverCapacity: Exit Function '一旦書き込み先をクリア' targetRange.ClearContents '振り番処理' Dim rowCount As Integer rowCount = UBound(roomDataArray, 1) Dim getFromArrayCount As Integer '配列にアクセスした回数を記録する変数' getFromArrayCount = 1 Dim loopOfArray As Integer Dim i As Integer For i = 1 To targetRange.Count '配列からの値取得が何周目かを計算する' loopOfArray = (getFromArrayCount - 1) \ rowCount + 1 With targetRange.Cells(i, 1) '振り番しようとする部屋が定員に達していたら次の部屋に進める' Do While loopOfArray > _ roomDataArray(((getFromArrayCount - 1) Mod rowCount + 1), 2) getFromArrayCount = getFromArrayCount + 1 loopOfArray = (getFromArrayCount - 1) \ rowCount + 1 Loop .Value = roomDataArray(((getFromArrayCount - 1) Mod rowCount + 1), 1) getFromArrayCount = getFromArrayCount + 1 End With Next assignOfRooms = assignSuccessed Exit Function errorHandler: assignOfRooms = failedByUnknownReason Debug.Print Err.Number & ":" & Err.Description End Function
列挙体の変更
検出できる処理失敗の原因が増えたのに伴って、前回記事のリスト1を書き換える。
リスト3 標準モジュールの宣言セクション
Public Enum AssignRoomsResult assignSuccessed = True failedByMultipleColumns = 1 failedByNotTwoColumnsTable failedByIrregularRoomDataTable '……(*)' failedByOverCapacity failedByUnknownReason = 10 End Enum
新たに加えたのは、(*)の部分。リスト2の
If Not isAppropriateAsCapacityTabele(roomData:=roomData) Then _ assignOfRooms = failedByNotTwoColumnsTable: Exit Function
のところで、返り値として使用している。
使ってみる
次のコードで実験する。
リスト4 標準モジュール
Public Sub testassignOfRooms() Select Case assignOfRooms(targetRange:=Selection, _ roomData:=ActiveSheet.Range("A1").CurrentRegion, _ hasHeader:=True) Case assignSuccessed MsgBox "部屋割り完了!" Case failedByMultipleColumns Call makeUserSick("部屋割り結果を書き込む列が2列以上あるやんけぼけーwww") Case failedByNotTwoColumnsTable Call makeUserSick("部屋定員表が何で2列とちゃうねんぼけーwww") Case failedByIrregularRoomDataTable Call makeUserSick("部屋定員表が何かおかしいんじゃぼけーwww") Case failedByOverCapacity Call makeUserSick("定員オーバーじゃぼけーwww") Case failedByUnknownReason Call makeUserSick("何か知らんけど失敗したやんけぼけーwww") End Select End Sub
makeUserSickメソッドについては、コチラを参照のこと。
部屋定員表の人数欄に「ち~んw」とか入っているのでこうなる。
部屋定員表の人数欄に小数があったりするからこうなる。
定員オーバーにも引き続き対応できている。
おわりに
メインのFunctionの行数があまり減っていないのがアレですけど、コードを見直すときの一つの方針かな、と。