オブジェクト用マージソート

自身の勉強も兼ねてオブジェクト用のマージソートを作成してみた。

> [マージソート](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%BC%E3%82%B8%E3%82%BD%E3%83%BC%E3%83%88)

---

190509追記

修正版を作成しています。
[imihito_Sandbox/M_MergeSort.bas at master · imihito/imihito_Sandbox](https://github.com/imihito/imihito_Sandbox/blob/master/VBA_General/M_MergeSort.bas "imihito_Sandbox/M_MergeSort.bas at master · imihito/imihito_Sandbox")

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">以下の記事の関数を、より汎用化・高速化したマージソート関数を作った記憶があるけれどどこにやったっけ…<br><br>オブジェクト用マージソート <a href="https://t.co/HuQTY0SZIz">https://t.co/HuQTY0SZIz</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; いみひと (@nukie_53) <a href="https://twitter.com/nukie_53/status/1126248540060012544?ref_src=twsrc%5Etfw">May 8, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

---

# コード

```vb
Option Explicit

'ソート用ユーザー定義型
Private Type SortElement
    Object As Object
    Value As Variant
End Type

'引数
    'Objects    ：ソートしたいオブジェクトのVBA.Collection
    'MemberName ：プロパティ（VbGet）やメソッド（VbMethod）の名前。
    'CallType   ：省略可。MemberNameの種類。VbGetもしくはVbMethod。省略時VbGet。
    'Ascending  ：省略可。Trueなら昇順、Falseなら降順。省略時True。
    
'戻り値
    'ソートされたVBA.Collection

Public Function ObjectSort(ByVal Objects As VBA.Collection, _
                           ByVal MemberName As String, _
                           Optional ByVal CallType As VbCallType = VbGet, _
                           Optional ByVal Ascending As Boolean = True _
                        ) As VBA.Collection
    
    Select Case CallType
        Case VbLet, VbSet
            Call Err.Raise(vbObjectError, "ObjectSort", "CallTypeにはVbGetもしくはVbMethodを指定してください")
    End Select
    
    Dim basArray() As SortElement
    ReDim basArray(1 To Objects.Count)
    
    Dim i&, obj As Object
    For Each obj In Objects
        i = i + 1
        Set basArray(i).Object = obj
        Let basArray(i).Value = VBA.CallByName(obj, MemberName, CallType)
    Next obj
    
    'コピーを作成。ちゃんと考えれば領域確保だけでも良いかも。
    Dim OutArray() As SortElement
    OutArray = basArray
    
    'ソート
    Call RecurseMergeSort(basArray, OutArray, 1, Objects.Count, Ascending)
    
    Erase basArray
    
    '出力用に入れ直し
    Dim oCol As VBA.Collection
    Set oCol = New VBA.Collection
    For i = 1 To Objects.Count
        oCol.Add OutArray(i).Object
    Next i
    
    Set ObjectSort = oCol
    
End Function


Private Sub RecurseMergeSort(ByRef InptArray() As SortElement, _
                             ByRef OutArray() As SortElement, _
                             ByVal Start As Long, _
                             ByVal Length As Long, _
                             ByVal Ascending As Boolean)
    
    Dim halfLen As Long
    halfLen = VBA.CLng(Length / 2)
    
    '前半のソート
    If halfLen >= 2 Then
        Call RecurseMergeSort(OutArray, InptArray, Start, halfLen, Ascending)
    End If
    
    '後半のソート
    If Length - halfLen >= 2 Then
        Call RecurseMergeSort(OutArray, InptArray, Start + halfLen, Length - halfLen, Ascending)
    End If
    
    '前半部分の添え字と最大値
    Dim lwIndex As Long:    lwIndex = Start
    Dim lwMax As Long:      lwMax = Start + halfLen - 1
    
    '後半部分の添え字と最大値
    Dim upIndex As Long:    upIndex = Start + halfLen
    Dim upMax As Long:      upMax = Start + Length - 1
    
    '全体の添え字と最大値
    Dim oIndex As Long:     oIndex = Start
    Dim oMax As Long:       oMax = Start + Length - 1
    
    Dim leftIndex As Long   '余り用
    
    Dim flg As Boolean
    
    For oIndex = Start To oMax Step 1
        '値が同じなら順番維持
        flg = (InptArray(lwIndex).Value = InptArray(upIndex).Value)
        
        '値が同じじゃない場合、再判定
        If Not flg Then flg = (Ascending = (InptArray(lwIndex).Value < InptArray(upIndex).Value))
        
        If flg Then
            OutArray(oIndex) = InptArray(lwIndex)
            If lwIndex = lwMax Then
                leftIndex = upIndex
                Exit For
            Else
                lwIndex = lwIndex + 1
            End If
        Else
            OutArray(oIndex) = InptArray(upIndex)
            If upIndex = upMax Then
                leftIndex = lwIndex
                Exit For
            Else
                upIndex = upIndex + 1
            End If
        End If
    Next oIndex
    
    'Next oIndexを飛ばした分インクリメントする
    For oIndex = oIndex + 1 To oMax Step 1
        OutArray(oIndex) = InptArray(leftIndex)
        leftIndex = leftIndex + 1
    Next oIndex
    
End Sub
```

# 作成にあたって

`CallByName`関数を用いてソート用の値を取得するが、都度参照の方が処理が重いイメージがあった（未確認）。
そのため、値は最初の段階で取得しておき、以降は取得した値を使うようにした。
オブジェクトと値をまとめて扱うための型として、ユーザー定義型`SortElement`を定義した。

ソートの際は、配列を二つ用意し、再帰の中で相互に転記する形にした。

# イケてないところ

## VBA.Collection<->配列間の転記

`RecurseMergeSort`の実装をCollectionにできないか試してみたい（その場合、クラスモジュールの追加が必要だが）。

# 参考

> この記事を読まなかったらマージソートという言葉すら知らなかったと思われる。
> [VBA マージソートの実装と図解](http://thom.hateblo.jp/entry/2016/03/21/120449)