VB: 2次元配列を行の単位で処理する

XSLTとはまるで縁のないVisual Basicの話です.数年前会社が新しい社員を募集したとき、面接に参加したら、来ていた人は「Visual Basicできます」でした.「VBってC#C++やっている人からするとディスられますよね?」って聞いたら「まあそういうこともありますね」だったと思います.今の会社はC++一辺倒で、他に有ってもJavaくらいなので、(たぶん)私が「Visual Basicでできます!」っていったら間違いなくMLでボロクソに言われるか、スリッパが飛んでくる感じです.

私事で恐縮ですが、Visual Basicどうこう以前のMS-DOSの時代にQuick Basic 4.5というのがありました.初めてフリーになったときに仕事を探しにいったら「一本5万円で組んでみますか?」と言われて、自分で購入して使い始めました.Quick Basicだったのは、その相手先の会社さんが、その前は「スパゲティプログラム量産間違いなし」のN88 BASICでやってたからなんじゃないかと思います.しかしQB45は廉価版ゆえコンパイラの品質も良くなく、よくこけました.そこでもうちょっと高価版のコンパイラを搭載したMicrosoft Basic Professional Development Systemというのを買って乗り換え、仕事はほぼBasic一辺倒でした.DOSからWindowsに代わったときにMSから発売されたVisual Basicはお試し英語版だったと思いますが、DOSでは味わえなかったカッコイイフォントでGUIでコードが表示されて感激したものでした.
そのVisual Basicも今年の3月にMSの.NETのチームのブログで

We Do Not Plan to Evolve Visual Basic as a Language

「言語としてVisual Basicを進化させる予定はありません」

と発表されてからは、もうVBも終わりなのか?と悲しくなったりしますが、VB自体は2016年には25年の誕生日を迎えて久しいので、言語自体はもうそれほど進化する必要もないのでは?なんて思ったりします.

さて本題、実は次のようなExcelのワークシートから、VSTOで作ったアドインでシートの内容を配列に読み取り、意味のあるデータに加工しようと思います.

f:id:toshi_xt500:20200823000701p:plain
取り出してデータに加工したいExcelのシート

そうするとかならず使わねばならないのが2次元の配列です.実は配列苦手です.あたりまえなのですが、そもそも行と列のインデックスを指定しないと要素にはアクセスできません.あながちでてくるのが2段のForループです.

例えば、データ行をmDataRowという二次元配列に無事格納した後、空の行を調べるとすると

        Dim IsEmpty As Boolean
        For i = 0 To mDataRow.GetLength(0) - 1
            IsEmpty = True
            For j = 0 To mDataRow.GetLength(1) - 1
                If mDataRow(i, j).value <> "" Then
                    IsEmpty = False
                End If
            Next
            If IsEmpty Then
                Debug.WriteLine("Empty row no=" & i)
            End If
        Next

なんていうForループが出てくるに決まっているからです.(そしてこういうコードを平気で書いていると永久にディスられると思うんですが...)

考えたのは、こんな昔の書き方とは違って

  • 行や列単位で2次元配列にアクセスできるようにし
  • LINQを使って、もっと「スマート」に書けないか?

ということです.贅沢ですかね?

で普段はVBなんて数年間やっていないのでWebを探したら、「拡張メソッド」というのがありました.実は上記のmDataRowの要素はCellInfoというクラスのインスタンスなんですが、これを一般化して、array As T(,)の行をIEnumrableとして取り出せる拡張メソッドを書いてみました.以下のような感じです.

Imports System.Runtime.CompilerServices

''' <summary>
'''  Extend two dimensional array : get IEnumerble or list of row 
''' </summary>
Public Module ArrayEx

    <Extension()>
    Public Iterator Function GetRow(Of T)(ByVal array As T(,), ByVal row As Integer) As IEnumerable(Of T)
        For i As Integer = 0 To array.GetLength(1) - 1
            Yield array(row, i)
        Next
    End Function

    <Extension()>
    Public Iterator Function GetRows(Of T)(ByVal array As T(,)) As IEnumerable(Of IEnumerable(Of T))
        For i As Integer = 0 To array.GetLength(0) - 1
            Yield array.GetRow(i)
        Next
    End Function

    <Extension()>
    Public Function GetRowsAsList(Of T)(ByVal array As T(,)) As List(Of IEnumerable(Of T))
        Dim list As List(Of IEnumerable(Of T)) = New List(Of IEnumerable(Of T))
        For i As Integer = 0 To array.GetLength(0) - 1
            list.Add(array.GetRow(i))
        Next
        Return list
    End Function

End Module

こんな風に定義しておくと、空の行を調べるプログラムはLINQ+拡張メソッドを使って次のように書けます.

        Dim rows As IEnumerable(Of IEnumerable(Of ATLCellInfo)) = DirectCast(mDataRow, CellInfo(,)).GetRows
        Dim emptyRowIndex2 = rows _
                             .Select(Function(row, i) New With {.Row = row, .Index = i}) _
                             .Where(Function(aci) aci.Row.All(Function(cell) cell.value = "")) _
                             .Select(Function(aci) aci.Index)
        For Each i As Integer In emptyRowIndex2
            Debug.WriteLine("Empty row no=" & i)
        Next

例えば結果は

Empty row no=5
Empty row no=29

なんて出てくれます.

エッ?Forループで回した方が簡単で早いだって?確かにそうかもしれません.ただ拡張メソッドを定義しておくだけで、LINQで一発で問い合わせのように書いて結果が得られるところがいいと思います.この例だと、「何番目」というのを出さねばならなかったので、クェリ構文でなくメソッド構文しか書き方がわかりませんでした.だからちょっと冗長.でももし拡張メソッドがなければ、いくら望む処理関数や手続きにカプセル化してもその中で毎回のようにForループで回すオンパレードになってしまいますよね.これではウン十年前から進化がありません.