
Excelでの行列計算は便利なんだけど、セルが跨いでる値を配列として計算したい場合、飛んでるセルを束ねて整理する列を作ったりする必要がある。こんな中間セルを作るのは面倒なんで、複数のセルを結合して配列を作る関数が無いか探してみたんだけど無いみたいだった。(Microsoftは、VBAで配列は使わせたくない節がある。。。)
仕方が無いんでCreateVectorHっていう、sum関数みたいなノリで領域指定したら、引数をHorizontal配列にして返す関数を作った。Horizontal配列作る関数作ったら、Vertical配列で戻す関数もセットで欲しいんで、CreateVectorVって関数でCreateVectorHをラップして、返ってきた水平方向の配列を転置しようと考えた。サクッと出来ると思ったんだけど、結構手間取った。
ユーザー定義関数(UDF)の可変長引数関数を、可変長引数関数でラップして作ろうとした場合、ParamArrayで受けたものをそのまま別のParamArrayに渡したら、Variant型の階層配列になってしまった。
受けた側は可変長引数の1つの要素として取り扱うんだから、当然っちゃー当然。
取り合えず、Range型か判定して処理するかと思ったもので調べてみたところ、VarType関数やTypeName関数を使うと判定できるっぽいんでこの辺参考にしつつテストしてみた。
Sub VarTest()
Dim i As Integer
Dim d As Double
Dim str As String
Dim bool As Boolean
Dim data As Date
Dim obj As Object
Dim v As Variant
Dim vArr() As Variant
Dim rng As Range
Dim strArr() As String
Dim iArr() As Integer
MsgBox "Integer:" & VarType(i) & ", " & TypeName(i) & vbCrLf & _
"Double:" & VarType(d) & ", " & TypeName(d) & vbCrLf & _
"String:" & VarType(str) & ", " & TypeName(str) & vbCrLf & _
"Boolean:" & VarType(bool) & ", " & TypeName(bool) & vbCrLf & _
"Date:" & VarType(data) & ", " & TypeName(data) & vbCrLf & _
"Object:" & VarType(obj) & ", " & TypeName(obj) & vbCrLf & _
"Variant:" & VarType(v) & ", " & TypeName(v) & vbCrLf & _
"Array Variant:" & VarType(vArr) & ", " & TypeName(vArr) & vbCrLf & _
"Range:" & VarType(rng) & ", " & TypeName(rng) & vbCrLf & _
"Array String:" & VarType(strArr) & ", " & TypeName(strArr) & vbCrLf & _
"Array Integer:" & VarType(iArr) & ", " & TypeName(iArr)
End Sub

結果を見ると、細かい型は判定できないみたい。VarTypeだと、RangeはObject型として判定された。(戻り値9)
これでRangeとVariantの判定が出来るかと思ったんだけど、Variantは何も入れてないとEmptyが返るように、代入される値によって判定される型が変わるようだった。
また、Rangeみたいなclassは、代入先がint型かStringかなどの型式に応じて戻り値の型を変えることが多い。念のため、Excel Sheetから選択範囲がどの様に判定されるかテストしてみた。
試してみたのは下記のUDF。引数をAs Variantにしてるか、As Rangeにしてるかの違いだけ。
Function VarTypeTestVariant(Arr As Variant)
VarTypeTestVariant = VarType(Arr)
End Function
Function VarTypeTestRange(Arr As Range)
VarTypeTestRange = VarType(Arr)
End Function

黄色で塗りつぶした範囲のセルを上記関数に送ってみた。結果としては、VarTypeTestVariant(C3)という感じで送ると、単一セルの場合、空白セルは空、文字や数値はそれぞれIntなどの対応する型が返ってくるけど、VarTypeTestVariant(C6:D6)という感じで範囲選択した場合は、全てVariant型の配列という判定がされるみたい。
これだとParamArrayで受け取ったVariant型配列か、シートの選択範囲なのか判定できない。
仕方ないのでLBoundが走らなければ、Rangeという感じで型判定して処理することにした。
CreateVectorH, CreateVectorV
実装は下記の様な感じ。適当に再帰関数書いて実装しました。CreateVectorH使うと、EXCELのセルから配列作れるので、MMULTなどの行列計算が捗ります。
'https://memo-nikki.info/?p=1214
Function CountAllItem(ParamArray arr() As Variant)
Dim i As Long
Dim LB_Arr As Long, UB_Arr As Long
LB_Arr = LBound(arr)
UB_Arr = UBound(arr)
Dim LB_Arr1st As Long, UB_Arr1st As Long
Dim n_tmp As Long
CountAllItem = 0
If LB_Arr = UB_Arr Then '可変引数で送られた要素が1個だけ
If VarType(arr(0)) < vbArray Then 'この1個の要素が配列でなければ1を返す。
CountAllItem = 1
Exit Function
Else '可変引数で送られてきた要素が配列なら、
On Error GoTo LabelMaybeRange
LB_Arr1st = LBound(arr(0))
UB_Arr1st = UBound(arr(0))
For i = LB_Arr1st To UB_Arr1st
n_tmp = CountAllItem(arr(0)(i))
If n_tmp < 0 Then
GoTo LabelError
End If
CountAllItem = CountAllItem + n_tmp
Next
End If
Else
For i = LB_Arr To UB_Arr
n_tmp = CountAllItem(arr(i))
If n_tmp < 0 Then
GoTo LabelError
End If
CountAllItem = CountAllItem + n_tmp
Next
End If
Exit Function
LabelMaybeRange:
On Error GoTo LabelError
CountAllItem = arr(0).Count
Exit Function
LabelError:
Debug.Print "CountAllItemに送られた、データは配列でなく、Count要素も持っていない要素が含まれている。"
CountAllItem = -1
End Function
Function CountAllItem_Help()
CountAllItem_Help = "CountAllItem(ParamArray arr() As Variant, ...)" & vbCrLf _
& "引数はsum関数と同じ感じで指定。階層配列を探索し、引数群が持っている要素数の合計を返す。" & vbCrLf _
& "イレギュラーな要素が含まれていた場合、Errorで-1を返す。"
End Function
'https://memo-nikki.info/?p=1214
Function ArrayCopy(ByRef TgtArr() As Variant, ByVal StartPos As Integer, ByRef RefArr() As Variant)
Const strErrorArrayCopy As String = "<Error,ArrayCopy>"
Dim i As Long
Dim LB_TgtArr As Long, UB_TgtArr As Long
Dim LB_RefArr As Long, UB_RefArr As Long
LB_TgtArr = LBound(TgtArr)
UB_TgtArr = UBound(TgtArr)
LB_RefArr = LBound(RefArr)
UB_RefArr = UBound(RefArr)
Dim tmp_Vec() As Variant, tmp As Variant
Dim EndPos_TgtArr As Long
EndPos_TgtArr = StartPos + UB_RefArr - LB_RefArr
If EndPos_TgtArr > UB_TgtArr Then
ReDim tmp_Vec(LB_TgtArr To EndPos_TgtArr)
Else
ReDim tmp_Vec(LB_TgtArr To UB_TgtArr)
End If
'前方
For i = LB_TgtArr To StartPos - 1
tmp_Vec(i) = TgtArr(i)
Next
'コピー部
Dim diff As Long
diff = LB_RefArr - StartPos
For i = StartPos To EndPos_TgtArr
tmp_Vec(i) = RefArr(i + diff)
Next
'後部
For i = EndPos_TgtArr + 1 To UB_TgtArr
tmp_Vec(i) = TgtArr(i)
Next
ArrayCopy = tmp_Vec
Exit Function
LabelError:
Debug.Print "ArrayCopyに送られた、データは配列でなく、Count要素も持っていない要素が含まれている。"
ArrayCopy = strErrorArrayCopy
End Function
Function ArrayCopy_Help()
ArrayCopy_Help = "ArrayCopy(ByRef TgtArr() As Variant, ByVal StartPos As Integer, ByRef RefArr() As Variant)" & vbCrLf _
& "TgtArrに対し、StartPosを開始点として、RefArrをコピーした配列を返す。" & vbCrLf _
& "戻す配列は、TgtArrとは別に作成されるので、送信元の配列は変化しない。" & vbCrLf _
& "イレギュラーな要素が含まれていた場合、""<Error,ArrayCopy>""を返す。" & vbCrLf _
& "本関数はEXCEL Sheet上からRangeを送っても使えない。sheetからならCreateVectorHでこと足りるはず。"
End Function
'https://memo-nikki.info/?p=1214
Function CreateVectorH(ParamArray arr() As Variant)
Const strErrorVecH As String = "<Error,CreateVectorH>"
Dim n_Vec As Long, n As Long, i As Long, j As Long
Dim LB_Arr As Long, UB_Arr As Long
LB_Arr = LBound(arr)
UB_Arr = UBound(arr)
Dim obj, tmp() As Variant
Dim n_tmp As Long
Dim tmp_Vec() As Variant
n_Vec = CountAllItem(arr)
If n_Vec < 0 Then
GoTo LabelError
End If
ReDim tmp_Vec(n_Vec - 1)
n = 0
Dim LB_Arr1st As Long, UB_Arr1st As Long
If LB_Arr = UB_Arr Then '可変引数で送られた要素が1個だけ
If VarType(arr(0)) < vbArray Then 'この1個の要素が配列でなければこの要素を返す。
CreateVectorH = arr(0)
Exit Function
Else '可変引数で送られてきた要素が配列なら、
On Error GoTo LabelMaybeRange
LB_Arr1st = LBound(arr(0))
UB_Arr1st = UBound(arr(0))
For i = LB_Arr1st To UB_Arr1st
If VarType(arr(0)(i)) < vbArray Then '配列でなければそのまま代入。
tmp_Vec(n) = arr(0)(i)
n = n + 1
Else '配列なら
tmp = CreateVectorH(arr(0)(i)) '整理された配列を取得
tmp_Vec = ArrayCopy(tmp_Vec, n, tmp)
n = n + 1 + UBound(tmp)
End If
Next
GoTo LabelMaybeRangeEnd
LabelMaybeRange:
On Error GoTo LabelError
For Each obj In arr(0)
If VarType(obj) < vbArray Then '配列でなければそのまま代入。
tmp_Vec(n) = obj
n = n + 1
Else '配列なら
tmp = CreateVectorH(obj) '整理された配列を取得
tmp_Vec = ArrayCopy(tmp_Vec, n, tmp)
n = n + 1 + UBound(tmp)
End If
Next
LabelMaybeRangeEnd:
End If
Else
For i = LB_Arr To UB_Arr
If VarType(arr(i)) < vbArray Then '配列でなければそのまま代入。
tmp_Vec(n) = arr(i)
n = n + 1
Else '配列なら
tmp = CreateVectorH(arr(i)) '整理された配列を取得
tmp_Vec = ArrayCopy(tmp_Vec, n, tmp)
n = n + 1 + UBound(tmp)
End If
Next
End If
CreateVectorH = tmp_Vec
Exit Function
LabelError:
Debug.Print "CreateVectorHに送られた、データは配列でなく、Count要素も持っていない要素が含まれている。"
CreateVectorH = strErrorVecH
End Function
Function CreateVectorH_Help()
CreateVectorH_Help = "CreateVectorH(ParamArray arr() As Variant, ...)" & vbCrLf _
& "sum関数と同じ感じで指定した引数を、水平方向の配列にして返す。"
End Function
'https://memo-nikki.info/?p=1214
Function CreateVectorV(ParamArray ArrRngTgt() As Variant)
CreateVectorV = WorksheetFunction.Transpose(CreateVectorH(ArrRngTgt))
End Function
Function CreateVectorV_Help()
CreateVectorV_Help = "CreateVectorV(ParamArray arr() As Variant, ...)" & vbCrLf _
& "sum関数と同じ感じで指定した引数を、垂直方向の配列にして返す。"
End Function
例えば、下図のB列ような感じで、簡単にN次関数が書けちゃいます。

意味合い的には、A2^{0,1,2}という感じの表現です。デフォルト機能だと、{0,1,2}みたいに乗数の配列を書くか、C列みたいに逐次乗数を選択する形になります。{0,1,2}のような配列の作成は定数のみで、セルの値から作成できません。また、逐次乗数を選択するのは、数増えると見にくいし、書くのも大変なのでやりたくありません。
CreateVectorHを使うとこれが簡単に実現できるので、めちゃくちゃ楽になりました。
これで行列計算がはかどります。
ふと思いましたが、CreateVectorH使うと、可変長引数、多次元配列、階層配列、Range、この辺ごちゃまぜでも、全ての要素を単純な1次元配列として返すので、構造を気にしない可変長引数の関数をラップしたUDF作りたいときは、取り合えず、受けた引数をCreateVectorHにぶっこんで、1次元Variant配列にしてから処理しちゃえば話が簡単になりそうです。


コメント