部屋割りマクロ(Excel)のリファクタリング(3)

部屋割りマクロのリファクタリング

傍流の処理を外に出す

前回の

akashi-keirin.hatenablog.com

リスト2では、大まかに言って

  1. 引数のチェック
  2. 定員表の配列化
  3. 振り番処理

の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メソッドについては、コチラを参照のこと。

f:id:akashi_keirin:20180218105729j:plain

部屋定員表の人数欄に「ち~んw」とか入っているのでこうなる。

f:id:akashi_keirin:20180218105723j:plain

部屋定員表の人数欄に小数があったりするからこうなる。

f:id:akashi_keirin:20180218105741j:plain

定員オーバーにも引き続き対応できている。

おわりに

メインのFunctionの行数があまり減っていないのがアレですけど、コードを見直すときの一つの方針かな、と。

@akashi_keirin on Twitter