この投稿は以前のものです
大幅にクラスの内容を刷新した上で書き直したため、こちらをご覧ください。
https://qiita.com/es2/private/04120886c089608d207e
概要
VBAの動的配列にあたるCollectionは要素の自乗倍重くなりArrayListは.netの新しいバージョンだと使えない
拾ってきたListは配列を毎回Redim(遅い)し使いたいメソッドもなかったためまともな?Listを作ってみました!
※このListはStringやLongなどプリミティブな型にしか対応しておりません。参照型はまた今度
2020/12/30追記:StringBuilder的な高速結合とプラス要素間にデリミタ文字列を付与する機能追加
クラスだけ欲しい方は下の
ここから下をコピーして Listという名前のクラスモジュールに入れてくれい!!からコピペしてね
例えばこういうことが出来ます!!
Dim List as List:set List as New List
List.Add(1) '要素を追加します
call List.AddRange(Range("A1:A1048576").Value) '複数要素を追加します。
List.OrderBy'リストの内容をソートします
arr= List.ToArray '配列を返します。つまり for each elm in List.Toarrayなどとかけます。
set newList = List.DistinctToNewList '内容重複がない新しいリストを返します(Win限定追加機能)
set newList2 = List.PopRange(1,10)'1-10の要素を新しいリストで返し元のリストから消します
str = List.ToBuildStringWithDelimiter(",")'要素をカンマで区切って高速結合(100万個で&結合の1000倍超速)
出来ること
基本
Add:値を追加します。これは値の数が増えれば増えるほどCollectionより早いです。
AddRange:イテレート可能な(for eachができる)値を追加していきます。Listは専用メソッド有
ToArray:配列として返します。要素数はデータ数に切り詰められます。0スタートです。
OrderBy/SortByAscending:値を昇順ソート。降順はSortByDescending/OrderByDescending
Count:格納されている個数を取得します。indexの最大値はCount -1よ?
準基本
ConcatList:AddRangeのリスト用メソッドです。
First:最初の要素を返します。
Last:最後の要素を返します。
GetValueOfIndex:配列に対する(n)と同じです。
Remove:n番目の要素を削除して詰めます(あまりはやくない)
RemoveAt:引数に一致する値(=で一致するもの)を削除します
RemoveAll:引数に一致するもの(=で一致するもの)を全て削除します。この名前でいいのか?
Pop:n番目の要素を取得して、リストからその値を消して前に詰めます
PopAt:引数に一致する値(=で一致するもの)を取得してリストからその値を消して前に詰めます
Clone:リストを複製して返します。
Reverse:格納されている値を反転します。
Randamize:値をシャッフルします。精度はいまいちだと思います
応用 (Range系のメソッドで範囲外を指定した場合はコメント参照)
TakeToNewList:前方からn個要素を取得して新しいリストを返します。
SkipToNewList:指定数をスキップしてそれ以降を抜き出します。
GetRange指定範囲の値を別のリストとして返します
RemoveRange:指定範囲を削除して前に詰めます(要検証)
PopRange:指定範囲を取得して、リストからその値を消して前に詰めます(要検証)
StringContains:値を文字列で検索してBoolを返します。
ToBuildString:要素を全て結合した文字列を返します(100万個結合で&結合の1000倍以上早かった)
ToBuildStringWithDelimiter:要素間に引数の文字列を追加して結合した文字列を返します。
Math系のメソッド
※数値が入っていること前提です。下の方にあるので必要なら追加してください。
WorkSheetFunctionはセルに入っている値以外だと個数制限がある。
Sum,Average,Median,Max,Min,StDevP
Windowsでないと動かないと思われる機能
※下の方にあるので必要なら追加してください。
DistinctToNewList:ユニーク(内容で重複しているものがない状態)の新しいリストを返します
StringContains_RegExp:値を正規表現で検索してBoolを返します。
ModeSingle:最頻値を1つ取得します。数値が入っていること前提です
ModeMulti:最頻値を配列で取得します。数値が入っていること前提です
バグなどはコメントで教えてくれると嬉しいです(直るとはいっていない)
参照型はWhere,Selectぐらいは作りたいなと思って途中で放置中です。メンドクサイ
ここから下をコピーして Listという名前のクラスモジュールに入れてくれい!!
Option Base 0 'クラス内の処理で1の方が都合がいいが、ToArrayした場合の返り値などにも影響を及ぼすので・・・
Private myArr As Variant 'ここに値が入ります(cloneの仕様のためローカル変数全部普通に触れます(あぶない))
Private myCount As Long '配列の要素数です。
Private myCapacity As Long '配列の最大サイズです。
Property Get arr() As Variant: arr = myArr: End Property
Property Let arr(arg): myArr = arg: End Property
Property Get Count() As Long: Count = myCount: End Property
Property Let Count(arg As Long): myCount = arg: End Property
Property Get Capacity() As Long: Capacity = myCapacity: End Property
Property Let Capacity(arg As Long): myCapacity = arg: End Property
'コンストラクタです。クラスの初期化を行います。 配列の初期サイズ決めたかったのですが、仕様上引数渡せないみたいです()
Private Sub Class_Initialize()
myCapacity = 0
ReDim myArr(myCapacity)
End Sub
'デストラクタです、メモリを開放します。
Private Sub Class_Terminate()
Set myArr = Nothing
VBA.DoEvents
End Sub
'クラスを初期化します。
Public Function Clear()
myCount = 0
Call Class_Initialize
End Function
'値をリストに追加します。
'動作速度とメモリの観点から、格納アイテム数が配列のキャパシティを超えた場合のみReDim Preserveを行いキャパシティを倍にします。
Public Sub Add(val)
If myCount > myCapacity Then
myCapacity = (myCapacity + 1) * 2 - 1
ReDim Preserve myArr(myCapacity)
End If
myArr(myCount) = val
myCount = myCount + 1
End Sub
'配列若しくは、イテレート可能なオブジェクトを追加します。call を先頭に付けないと動かないぽいです。ハマりました。
Public Sub AddRange(args As Variant)
For Each arg In args
Me.Add (arg)
Next
End Sub
'2つのリストを結合します。引数に入れたほうがあとに来ます。
'シグネチャで型を指定すると、使用側で明示的にListの型を指定していない場合受けられないためVariantです。
'使用側でcallを付ける必要があります。
Public Sub ConcatList(argList As Variant)
If argList.Count = 0 Then Exit Sub
For Each elm In argList.ToArray
Me.Add (elm)
Next
End Sub
'最初の要素を取得します。
Public Function First(): First = IIf(myCount = 0, Empty, myArr(0)): End Function
'最後の要素を取得します。
Public Function Last(): Last = IIf(myCount = 0, Empty, myArr(myCount - 1)): End Function
'インデックス値に対応する値を取得します。配列の[n]と同じです。
Public Function GetValueOfIndex(index As Long)
If index > myCount Then
GetValueOfIndex = Empty
Else
GetValueOfIndex = myArr(index)
End If
End Function
'特定の範囲を抜き出して新しいリストとして返します。(minIndex<=抜き出すもの<=maxIndex)
'Minが不当に小さい、Maxが不当に大きい場合、最小インデックス、最大インデックスまでの要素を対象とします。
Public Function GetRange(minIndex As Long, maxIndex As Long) As List
minIndex = IIf(minIndex < LBound(myArr), LBound(myArr), minIndex)
maxIndex = IIf(maxIndex > myCount - 1, myCount - 1, maxIndex)
If minIndex > maxIndex Then
Set GetRange = New List
Exit Function
End If
Dim newlist As List: Set newlist = New List
For i = minIndex To maxIndex
newlist.Add (myArr(i))
Next i
Set GetRange = newlist
End Function
'indexに対応する要素を削除してデータを前に詰めます。効率はなんとなく悪そうかなと
Public Sub Remove(index As Long)
If myCount = 0 Then Exit Sub
If index < LBound(myArr) Or UBound(myArr) < index Then Exit Sub
For i = index To myCount - 1
myArr(i) = myArr(i + 1)
Next i
myCount = myCount - 1
myArr(myCount) = Empty
End Sub
'一番最初に引数の数値に引っかかったものを削除してデータを前に詰める
Public Sub RemoveAt(val)
If myCount = 0 Then Exit Sub
Dim i As Long
For i = LBound(myArr) To myCount - 1
If myArr(i) = val Then
Call Remove(i)
Exit Sub
End If
Next i
End Sub
'引数の数値に一致するものを全て削除してデータを前に詰める というかどうせ全部走査して前に詰めるならリスト作り直す。
Public Sub RemoveAll(val)
Dim newlist As List: Set newlist = New List
Dim buf
For i = LBound(myArr) To myCount - 1
buf = myArr(i)
If buf <> val Then
newlist.Add (buf)
End If
Next i
myArr = newlist.arr
myCount = newlist.Count
myCapacity = newlist.Capacity
End Sub
'動くけどあやしい・・・
'特定の範囲を削除して詰めます。(minIndex<=ここ消える<=maxIndex)
'Minが不当に小さい、Maxが不当に大きい場合、最小インデックス、最大インデックスまでの要素を対象とします。
Public Sub RemoveRange(minIndex As Long, maxIndex As Long)
minIndex = IIf(minIndex < LBound(myArr), LBound(myArr), minIndex)
maxIndex = IIf(maxIndex > myCount - 1, myCount - 1, maxIndex)
If minIndex > maxIndex Then Exit Sub
Dim difference: difference = maxIndex - minIndex
For i = minIndex To myCount - 1
Dim swapIndex: swapIndex = i + difference + 1
If swapIndex >= myCount Then Exit For
myArr(i) = myArr(swapIndex)
Next i
For j = myCount - difference - 1 To myCapacity
myArr(j) = Empty
Next j
myCount = myCount - difference - 1
End Sub
'indexに対応する要素を返した上で、リストからその要素を削除します。
Public Function Pop(index As Long)
If index < LBound(myArr) Or UBound(myArr) < index Then
Pop = Empty: Exit Function
End If
Pop = myArr(index)
Call Remove(index)
End Function
'引数の値に最初一致したものを取得後削除します。
Public Function PopAt(val)
If myCount = 0 Then Exit Function
Dim i As Long
For i = LBound(myArr) To myCount - 1
If myArr(i) = val Then
PopAt = myArr(i)
Call Remove(i)
Exit Function
End If
Next i
End Function
'特定の範囲を返すと同時に削除して詰めます。(minIndex<=ここ消える<=maxIndex)
'Minが不当に小さい、Maxが不当に大きい場合、最小インデックス、最大インデックスまでの要素を対象とします。
Public Function PopRange(minIndex As Long, maxIndex As Long) As List
minIndex = IIf(minIndex < LBound(myArr), LBound(myArr), minIndex)
maxIndex = IIf(maxIndex > myCount - 1, myCount - 1, maxIndex)
If minIndex > maxIndex Then
Set PopRange = New List
Exit Function
End If
Dim difference: difference = maxIndex - minIndex
Dim result As List: Set result = New List
For index = minIndex To maxIndex
Call result.Add(myArr(index))
Next index
For i = minIndex To myCount - 1
Dim swapIndex: swapIndex = i + difference + 1
If swapIndex >= myCount Then Exit For
myArr(i) = myArr(swapIndex)
Next i
For j = myCount - difference - 1 To myCapacity
myArr(j) = Empty
Next j
myCount = myCount - difference - 1
Set PopRange = result
End Function
'配列に変換して返します。 配列の要素数はデータ数に切り詰められます。
Public Function ToArray()
Dim bufArr: bufArr = myArr
ReDim Preserve bufArr(myCount - 1)
ToArray = bufArr
End Function
'オブジェクトを複製(シャローコピー)します。
Public Function Clone() As List
Dim newlist As List: Set newlist = New List
newlist.arr = myArr
newlist.Count = myCount
newlist.Capacity = myCapacity
Set Clone = newlist
End Function
'指定数先頭から抜き出す
Public Function TakeToNewList(itemCount As Long) As List
'指定数が要素数を超えている場合クローンを返す
Dim minIndex As Long: minIndex = LBound(myArr)
If itemCount + 1 - minIndex > myCount Then
Set TakeToNewList = Me.Clone()
Else
Dim newlist As List: Set newlist = New List
For i = minIndex To itemCount - 1 + minIndex
newlist.Add (myArr(i))
Next i
Set TakeToNewList = newlist
End If
End Function
'指定数をスキップしてそれ以降を抜き出す
Public Function SkipToNewList(skipCount As Long) As List
'指定数が要素数を超えている場合nullのアイテムを返す
If skipCount > myCount Then
Set SkipToNewList = New List
Else
Dim newlist As List: Set newlist = New List
For i = skipCount To myCount - 1
newlist.Add (myArr(i))
Next i
Set SkipToNewList = newlist
End If
End Function
'昇順でソートします。(実装は多分クイックソートになっているはずです。ロジックいまいち理解していません。
Public Sub SortByAscending(): Call OrderBy: End Sub
Public Sub OrderBy()
If myCount < 1 Then Exit Sub
Call QuickSortAscending(LBound(myArr), myCount - 1)
End Sub
Private Sub QuickSortAscending(ByVal lowest As Long, ByVal highest As Long)
Dim l As Long: l = lowest
Dim h As Long: h = highest
Dim pivot As Long: pivot = myArr(Int((lowest + highest) \ 2))
Do
Do While myArr(l) < pivot
l = l + 1
Loop
Do While myArr(h) > pivot
h = h - 1
Loop
If l >= h Then Exit Do
Dim buf: buf = myArr(l)
myArr(l) = myArr(h)
myArr(h) = buf
l = l + 1
h = h - 1
Loop
If (lowest < l - 1) Then
Call QuickSortAscending(lowest, l - 1)
End If
If (highest > h + 1) Then
Call QuickSortAscending(h + 1, highest)
End If
End Sub
'降順でソートします。
Public Sub SortByDescending(): Call OrderByDescending: End Sub
Public Sub OrderByDescending()
If myCount < 1 Then Exit Sub
Call QuickSortDescending(LBound(myArr), myCount - 1)
End Sub
Private Sub QuickSortDescending(ByVal lowest As Long, ByVal highest As Long)
Dim l As Long: l = lowest
Dim h As Long: h = highest
Dim pivot: pivot = myArr(Int((lowest + highest) \ 2))
Do
Do While myArr(l) > pivot
l = l + 1
Loop
Do While myArr(h) < pivot
h = h - 1
Loop
If l >= h Then Exit Do
Dim buf: buf = myArr(l)
myArr(l) = myArr(h)
myArr(h) = buf
l = l + 1
h = h - 1
Loop
If (lowest < l - 1) Then
Call QuickSortDescending(lowest, l - 1)
End If
If (highest > h + 1) Then
Call QuickSortDescending(h + 1, highest)
End If
End Sub
'格納されている値を反転します。
Public Function Reverse()
If myCount < 1 Then Exit Function
Dim maxIndxNo As Long: maxIndxNo = myCount - 1
Dim buf(): ReDim buf(0 To maxIndxNo)
Dim counter As Long: counter = maxIndxNo
For i = 0 To maxIndxNo
buf(counter) = myArr(i)
counter = counter - 1
Next i
myArr = buf
End Function
' 値をシャッフルします。精度はいまいちかもしれません。精度が欲しければメルセンヌツイスターとか良いらしいので実装して下さい。自分は死なないならやりません。
Public Sub Randamize()
Dim maxIndxNo As Long: maxIndxNo = myCount - 1
Dim i As Long, rndIndex As Variant, tmp As Variant
For i = 0 To maxIndxNo
rndIndex = Int(maxIndxNo * Rnd)
tmp = myArr(i)
myArr(i) = myArr(rndIndex)
myArr(rndIndex) = tmp
Next
End Sub
'数値を文字列に変換し、その対象がリストに含まれるか検査します。
'containsの期待される動作的にワイルドカード対応のlikeより=の検査の方が適切かなと。contains_Likeみたいな方が良い?
Public Function StringContains(str As String) As Boolean
For Each elm In myArr
If str = CStr(elm) Then
StringContains = True
Exit Function
End If
Next
StringContains = False
End Function
'StringBuilderのようにコレクションを高速に結合してStringを返します。
Public Function ToBuildString() As String
Dim strCount As Long: strCount = 1
For Each elm In myArr
strCount = strCount + Len(elm)
Next
If strCount = 1 Then
ToBuildString = Empty
Exit Function
End If
Dim result As String: result = Space(strCount - 1)
Dim chrNumber As Long: chrNumber = 1
Dim i As Long: i = 1
For Each elm In myArr
Mid(result, chrNumber) = elm
chrNumber = chrNumber + Len(elm)
If i = myCount Then Exit For
i = i + 1
Next
ToBuildString = result
End Function
'StringBuilderのようにコレクションを高速に結合してStringを返します。
'要素間にdelimiterで指定した文字列を挿入します。
Public Function ToBuildStringWithDelimiter(delimiter As String)
Dim strCount As Long: strCount = 1
For Each elm In myArr
strCount = strCount + Len(elm)
Next
If strCount = 1 Then
ToBuildStringWithDelimiter = Empty
Exit Function
End If
Dim delimiterSize As Long: delimiterSize = Len(delimiter)
Dim result As String
result = Space(strCount + delimiterSize * myCount - 1)
Dim chrNumber As Long: chrNumber = 1
Dim i As Long: i = 1
For Each elm In myArr
Mid(result, chrNumber) = elm
Mid(result, chrNumber + Len(elm)) = delimiter
chrNumber = chrNumber + delimiterSize + Len(elm)
If i = myCount Then Exit For
i = i + 1
Next
ToBuildStringWithDelimiter = Left(result, Len(result) - delimiterSize)
End Function
以下は値が数値が格納されていることを前提とした計算メソッドです。必要であれば追加してください。
VBAのWorksheetFunctionは65536個より大きい値に適用できません(2次元配列にすれば1048576個までというのを聞いたことがありますが試していません)
'リストに数値しか入っていないこと前提のメソッド群です
'探しやすいようにだったり、邪魔にならないようにMathと頭につけた
'※実行効率よくない
'※※要素数が65536を超えるとworksheetfunctionは動かない
'合計値を取得します。
Public Function Math_Sum()
If myCount < 65535 Then
Math_Sum = WorksheetFunction.sum(myArr)
Else
Dim buf
For Each elm In myArr
buf = buf + elm
Next
Math_Sum = buf
End If
End Function
'平均値を取得します。
Public Function Math_Average()
If myCount < 65535 Then
Math_Average = WorksheetFunction.Average(myArr)
Else
Dim buf
Dim counter As Long: counter = 1
For i = LBound(myArr) To myCount - 1
buf = buf + myArr(i)
Next
Math_Average = buf / myCount
End If
End Function
'中央値を取得します。実行効率はうんこです・・・書き直して❤
Public Function Math_Median()
If myCount < 65535 Then
Math_Median = WorksheetFunction.Median(myArr)
Else
Dim newlist As List
Set newlist = Me.Clone
newlist.OrderBy
If (myCount Mod 2 = 0) Then
Math_Median = newlist.arr((newlist.Count) / 2 - 1)
Else
Math_Median = (newlist.arr(WorksheetFunction.RoundUp((newlist.Count) / 2, 0) - 1) _
+ newlist.arr(WorksheetFunction.RoundDown((newlist.Count) / 2, 0) - 1)) _
/ 2
End If
End If
End Function
'最大値を返します。
Public Function Math_Max()
If myCount < 65535 Then
Math_Max = WorksheetFunction.max(myArr)
Else
Dim max
For Each elm In Me.ToArray
max = IIf(elm > max, elm, max)
Next elm
Math_Max = max
End If
End Function
'最小値を返します。
Public Function Math_Min()
If myCount < 65535 Then
Math_Min = WorksheetFunction.Min(myArr)
Else
Dim Min: Min = myArr(0)
For Each elm In Me.ToArray
Min = IIf(elm < Min, elm, Min)
Next elm
Math_Min = Min
End If
End Function
'母集団の標準偏差を求めます
Public Function Math_StDevP()
If myCount < 65535 Then
Math_StDevP = WorksheetFunction.StDev(myArr)
Else
Dim buf As Variant
Dim sum As Double
Dim avg As Double
Dim v As Double
Dim arr: arr = Me.ToArray
For Each buf In arr
sum = sum + buf
Next
avg = sum / myCount
For Each buf In arr
v = v + (avg - buf) ^ 2
Next
Math_StDevP = Sqr(v / myCount)
End If
End Function
以下おそらくWindowsでしか動かない機能です。scripting.Dictionaryに関してはクラスで作っておられる方がいるらしいので
それを使用すれば実装できるかもしれません。(Win環境しか無く試しておりません)
https://github.com/VBA-tools/VBA-Dictionary
'一意の値(ユニーク値)にフィルターして新しいリストとして返します。
Public Function DistinctToNewList() As List
Dim dic As Object: Set dic = CreateObject("scripting.dictionary")
Dim newlist As List: Set newlist = New List
For Each elm In myArr
dic(elm) = 0
Next
For Each Key In dic.Keys
newlist.Add (Key)
Next
Set DistinctToNewList = newlist
End Function
'数値を文字列に変換し、対象文字列がリストに含まれるか正規表現で検査します。
'一応作っただけなので、どういう時に役に立つか考えていません。
Public Function StringContains_RegExp(Pattern As String) As Boolean
Dim regExp As regExp
Set regExp = CreateObject("VBScript.RegExp")
regExp.Pattern = Pattern
For Each element In myArr
If regExp.Test(element) Then
StringContains_RegExp = True
Exit Function
End If
Next
ContainsRegExp = False
End Function
'最頻値を1つとってきます。なんかworksheetfunction動きません。
'重複する値が極めて多いかほとんど重複しない場合はとても遅いです。
Public Function Math_ModeSingle()
Dim dic As Object: Set dic = CreateObject("scripting.dictionary")
'数値,個数の辞書を作る
For Each elm In Me.arr
If Not (IsEmpty(elm)) Then
dic(elm) = dic(elm) + 1
End If
Next
If dic.Count = 0 Then Exit Function
'最大の個数を調べる
Dim maxCounter As Long
Dim val As Long
For Each elm In dic.Items
maxCounter = IIf(maxCounter < elm, elm, maxCounter)
Next elm
'最大値と一致する値を返す
For Each elm In dic.Keys
If dic(elm) = maxCounter Then
Math_ModeSingle = elm
Exit Function
End If
Next
End Function
'最頻値を配列で返します。なんかworksheetfunction動きません。
'重複する値が極めて多いかほとんど重複しない場合はとても遅いです。
'もっと良い実装で誰か作って()
Public Function Math_ModeMulti()
Dim dic As Object: Set dic = CreateObject("scripting.dictionary")
'数値,個数の辞書を作る
For Each elm In Me.arr
If Not (IsEmpty(elm)) Then
dic(elm) = dic(elm) + 1
End If
Next
If dic.Count = 0 Then Exit Function
'最大の個数を調べる
Dim maxCounter As Long
Dim val As Long
For Each elm In dic.Items
maxCounter = IIf(maxCounter < elm, elm, maxCounter)
Next elm
'最大値と一致する値を入れる
Dim newlist As List: Set newlist = New List
For Each elm In dic.Keys
If dic(elm) = maxCounter Then
Call newlist.Add(elm)
End If
Next
Math_ModeMulti = newlist.ToArray
End Function